fengyanglei 4 mesi fa
parent
commit
cef7727e1c
4 ha cambiato i file con 1294 aggiunte e 14 eliminazioni
  1. 291 4
      README.md
  2. 170 10
      main.py
  3. 9 0
      requirements.txt
  4. 824 0
      test-chat.html

+ 291 - 4
README.md

@@ -1,5 +1,292 @@
-# AI智能体
+# AI Agent
 
-# 技术栈
-* 语言:Python
-* 框架:LangChain
+一个基于LangChain v1.1.0、FastAPI、OpenAI模型及 `create_agent` 智能体的聊天系统,支持流式输出。
+
+## 技术栈
+
+* **语言**: Python 3.10+
+* **框架**: 
+  - FastAPI - Web框架
+  - LangChain v1.1.0 - AI应用框架(最新版本)
+  - Uvicorn - ASGI服务器
+* **模型**: OpenAI Chat Completions (如 gpt-4o, qwen-plus)
+* **特性**: create_agent 智能体、工具调用、流式输出 (Server-Sent Events)
+
+## 功能特性
+
+- ✅ 基于LangChain v1.1.0框架
+- ✅ FastAPI RESTful API
+- ✅ OpenAI模型集成
+- ✅ create_agent 智能体 + 工具调用
+- ✅ 流式输出支持
+- ✅ 异步处理
+- ✅ API文档自动生成 (Swagger UI)
+
+## 安装步骤
+
+### 1. 克隆或下载项目
+
+```bash
+cd baoshi-ai-agent
+```
+
+### 2. 创建虚拟环境(推荐)
+
+```bash
+# Windows
+python -m venv venv
+venv\Scripts\activate
+
+# Linux/Mac
+python -m venv venv
+source venv/bin/activate
+```
+
+### 3. 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 4. 配置API密钥
+
+创建 `.env` 文件(在项目根目录),添加您的 OpenAI API 密钥:
+
+```env
+OPENAI_API_KEY=sk-xxxx
+```
+
+或者直接设置环境变量:
+
+```bash
+# Windows PowerShell
+$env:OPENAI_API_KEY="sk-xxxx"
+
+# Linux/Mac
+export OPENAI_API_KEY="sk-xxxx"
+```
+
+**获取API密钥**: 前往 [OpenAI 控制台](https://platform.openai.com/account/api-keys) 生成 API Key。
+
+## 运行应用
+
+### 方式1: 直接运行
+
+```bash
+python main.py
+```
+
+### 方式2: 使用Uvicorn(推荐)
+
+```bash
+uvicorn main:app --host 0.0.0.0 --port 8080 --reload
+```
+
+**注意**: 如果直接运行 `python main.py` 遇到启动错误,建议使用方式2(命令行启动),这种方式更稳定可靠。
+
+应用启动后,访问:
+- API文档: http://localhost:8080/docs
+- 替代文档: http://localhost:8080/redoc
+- 根路径: http://localhost:8080/
+
+## API接口
+
+### 1. 非流式聊天接口
+
+**POST** `/chat`
+
+请求体:
+```json
+{
+  "message": "你好,请介绍一下你自己",
+  "conversation_id": "default"
+}
+```
+
+响应:
+```json
+{
+  "content": "我是通义千问...",
+  "conversation_id": "default"
+}
+```
+
+### 2. 流式聊天接口
+
+**POST** `/chat/stream`
+
+请求体:
+```json
+{
+  "message": "写一首关于春天的诗",
+  "conversation_id": "default"
+}
+```
+
+响应:Server-Sent Events (SSE) 流式数据
+
+### 3. 根路径
+
+**GET** `/`
+
+返回API基本信息
+
+## 使用示例
+
+### Python示例
+
+```python
+import requests
+import json
+
+# 非流式聊天
+response = requests.post(
+    "http://localhost:8080/chat",
+    json={
+        "message": "你好",
+        "conversation_id": "test-001"
+    }
+)
+print(response.json())
+
+# 流式聊天
+response = requests.post(
+    "http://localhost:8080/chat/stream",
+    json={
+        "message": "写一首诗",
+        "conversation_id": "test-001"
+    },
+    stream=True
+)
+
+for line in response.iter_lines():
+    if line:
+        decoded_line = line.decode('utf-8')
+        if decoded_line.startswith('data: '):
+            data = decoded_line[6:]  # 移除 'data: ' 前缀
+            if data == '[DONE]':
+                break
+            print(data, end='', flush=True)
+```
+
+### cURL示例
+
+```bash
+# 非流式聊天
+curl -X POST "http://localhost:8080/chat" \
+  -H "Content-Type: application/json" \
+  -d '{"message": "你好", "conversation_id": "test-001"}'
+
+# 流式聊天
+curl -X POST "http://localhost:8080/chat/stream" \
+  -H "Content-Type: application/json" \
+  -d '{"message": "写一首诗", "conversation_id": "test-001"}' \
+  --no-buffer
+```
+
+### JavaScript示例
+
+```javascript
+// 流式聊天
+async function streamChat(message) {
+  const response = await fetch('http://localhost:8080/chat/stream', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({
+      message: message,
+      conversation_id: 'test-001'
+    })
+  });
+
+  const reader = response.body.getReader();
+  const decoder = new TextDecoder();
+
+  while (true) {
+    const { done, value } = await reader.read();
+    if (done) break;
+
+    const chunk = decoder.decode(value);
+    const lines = chunk.split('\n');
+    
+    for (const line of lines) {
+      if (line.startsWith('data: ')) {
+        const data = line.slice(6);
+        if (data === '[DONE]') return;
+        console.log(data);
+      }
+    }
+  }
+}
+
+streamChat('你好');
+```
+
+## 配置说明
+
+### OpenAI模型 & create_agent 智能体
+
+- `create_agent` 会将 LLM 与工具列表绑定成可调用的智能体。
+- 在 `main.py` 中通过 `ChatOpenAI` 配置模型(默认 `gpt-4o-mini`)。
+- 默认内置两个示例工具:
+  - `get_current_time`: 返回服务器当前时间。
+  - `summarize_project`: 输出项目简介(可替换为数据库/知识库查询)。
+- 可根据需要扩展更多工具,或更换模型名称:
+
+```python
+openai_model = ChatOpenAI(
+    model="gpt-4o",     # 支持 gpt-4o / gpt-4o-mini / gpt-4.1 等
+    temperature=0.7,
+    streaming=True,
+)
+
+agent = create_agent(
+    model=openai_model,
+    tools=tools,        # 自定义 Tool 列表
+    system_prompt="自定义系统提示",
+)
+```
+
+### 端口配置
+
+默认端口为 `8080`,可以在 `main.py` 中修改,或通过命令行参数指定:
+
+```bash
+# 命令行方式(推荐)
+uvicorn main:app --host 0.0.0.0 --port 8080
+
+# 或在 main.py 中修改端口号
+```
+
+## 注意事项
+
+1. 确保已正确设置 `OPENAI_API_KEY` 环境变量
+2. OpenAI 模型调用需要稳定的网络连接
+3. 流式输出使用Server-Sent Events (SSE)协议
+4. 建议在生产环境中使用HTTPS和适当的认证机制
+
+## 故障排除
+
+### 问题: API密钥错误
+
+**解决方案**: 检查 `.env` 文件或环境变量中的 `OPENAI_API_KEY` 是否正确设置。
+
+### 问题: 模块导入错误
+
+**解决方案**: 确保已安装所有依赖:
+```bash
+pip install -r requirements.txt
+```
+
+### 问题: 端口被占用
+
+**解决方案**: 修改 `main.py` 中的端口号,或关闭占用端口的程序。
+
+## 许可证
+
+MIT License
+
+## 贡献
+
+欢迎提交Issue和Pull Request!

+ 170 - 10
main.py

@@ -1,16 +1,176 @@
-# This is a sample Python script.
+"""
+AI Agent
+使用LangChain v1.1.0、FastAPI和OpenAI模型,支持流式输出
+"""
+import json
+import os
+from typing import AsyncIterator
+from dotenv import load_dotenv
+from fastapi import FastAPI
+from pydantic import BaseModel
+from langchain_openai import ChatOpenAI
+from langchain_core.messages import HumanMessage
+from sse_starlette.sse import EventSourceResponse
+from langchain.agents import create_agent
+from fastapi.middleware.cors import CORSMiddleware
 
-# Press Shift+F10 to execute it or replace it with your code.
-# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
+# 加载环境变量
+load_dotenv()
 
+# 初始化FastAPI应用
+app = FastAPI(title="AI Agent", version="1.1.0")
 
-def print_hi(name):
-    # Use a breakpoint in the code line below to debug your script.
-    print(f'Hi, {name}')  # Press Ctrl+F8 to toggle the breakpoint.
+# 添加CORS中间件
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],  # 在生产环境中应该设置具体的域名
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
 
+# 配置OpenAI模型
+# 需要设置环境变量 OPENAI_API_KEY
+# 可以通过 os.environ["OPENAI_API_KEY"] = "your-api-key" 设置
+# 或者通过 .env 文件配置
+openai_model = ChatOpenAI(
+    api_key=os.getenv('QWEN_API_KEY'),
+    base_url=os.getenv("OPENAI_BASE_URL"),
+    model=os.getenv("OPENAI_MODEL"),
+    temperature=0.7,
+    streaming=True,
+)
 
-# Press the green button in the gutter to run the script.
-if __name__ == '__main__':
-    print_hi('PyCharm')
+# create_agent 会根据提供的工具创建可调用的智能体
+agent = create_agent(
+    model=openai_model,
+    tools=[],
+    system_prompt="你是一名专业的AI助手,善于调用工具回答与时间和项目相关的问题。",
+)
 
-# See PyCharm help at https://www.jetbrains.com/help/pycharm/
+
+class ChatRequest(BaseModel):
+    """聊天请求模型"""
+    message: str
+    conversation_id: str = "default"
+
+
+class ChatResponse(BaseModel):
+    """聊天响应模型"""
+    content: str
+    conversation_id: str
+
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return {
+        "message": "AI Agent",
+        "version": "0.0.1"
+    }
+
+
+@app.post("/api/agent/chat", response_model=ChatResponse)
+async def chat(request: ChatRequest):
+    """非流式聊天接口"""
+    try:
+        # 创建消息并交给Agent
+        messages = [HumanMessage(content=request.message)]
+        response = await agent.ainvoke({"messages": messages})
+        print(f"结果: {response}")
+        return ChatResponse(
+            content=response["messages"][-1].content,
+            conversation_id=request.conversation_id
+        )
+    except Exception as e:
+        return ChatResponse(
+            content=f"错误: {str(e)}",
+            conversation_id=request.conversation_id
+        )
+
+
+def _extract_text(content_block) -> str:
+    """从LangChain内容块中提取纯文本"""
+    if content_block is None:
+        return ""
+
+    if isinstance(content_block, str):
+        return content_block
+
+    # LangChain 可能返回 list[dict] 或 list[ContentBlock]
+    text_parts = []
+    if isinstance(content_block, list):
+        for block in content_block:
+            if isinstance(block, str):
+                text_parts.append(block)
+            elif isinstance(block, dict):
+                if block.get("type") == "text":
+                    text_parts.append(block.get("text", ""))
+            else:
+                block_text = getattr(block, "text", None)
+                if block_text:
+                    text_parts.append(block_text)
+    else:
+        block_text = getattr(content_block, "text", None)
+        if block_text:
+            text_parts.append(block_text)
+
+    return "".join(text_parts)
+
+
+async def generate_stream(request: ChatRequest) -> AsyncIterator[str]:
+    """生成流式响应"""
+    try:
+        # 创建消息并流式执行Agent
+        messages = [HumanMessage(content=request.message)]
+
+        async for token, metadata in agent.astream(
+                {"messages": messages},
+                stream_mode="messages"
+        ):
+            text = _extract_text(getattr(token, "content", None))
+            if not text:
+                text = _extract_text(getattr(token, "content_blocks", None))
+
+            if not text:
+                continue
+
+            payload = json.dumps({"type": "text", "text": text}, ensure_ascii=False)
+            yield payload
+
+        # 发送结束标记
+        yield "[DONE]"
+    except Exception as e:
+        error_payload = json.dumps({"type": "error", "text": str(e)}, ensure_ascii=False)
+        yield error_payload
+        yield "[DONE]"
+
+
+@app.post("/api/agent/chat/stream", response_description='{"type": "text", "text": "你好"}')
+async def chat_stream(request: ChatRequest):
+    """流式聊天接口"""
+    return EventSourceResponse(generate_stream(request))
+
+
+if __name__ == "__main__":
+    import uvicorn
+    
+    # 检查API密钥
+    if not os.getenv("OPENAI_API_KEY"):
+        print("警告: 未设置 OPENAI_API_KEY 环境变量")
+        print("请设置环境变量或创建 .env 文件")
+    
+    # 启动服务器
+    # 注意: 如果直接运行仍有问题,建议使用命令行: uvicorn main:app --host 0.0.0.0 --port 8080
+    try:
+        uvicorn.run(
+            "main:app",  # 使用字符串形式更兼容
+            host="127.0.0.1",
+            port=8080,
+            log_level="info",
+            reload=False  # 禁用reload以避免某些兼容性问题
+        )
+    except Exception as e:
+        print(f"启动错误: {e}")
+        print("\n建议使用命令行启动:")
+        print("uvicorn main:app --host 0.0.0.0 --port 8080")

+ 9 - 0
requirements.txt

@@ -0,0 +1,9 @@
+fastapi>=0.104.1
+uvicorn[standard]>=0.30.0
+langchain>=1.1.0
+langchain-community>=0.3.0
+langchain-openai>=0.1.0
+pydantic>=2.5.0
+sse-starlette>=1.8.2
+python-dotenv>=1.0.0
+

+ 824 - 0
test-chat.html

@@ -0,0 +1,824 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>WMS智能助手</title>
+</head>
+<body>
+    <div class="warehouse-chat">
+        <!-- 侧边栏 -->
+        <div class="sidebar">
+            <div class="logo">
+                <div class="logo-icon">
+                    <span style="font-size: 20px;">🤖</span>
+                </div>
+            </div>
+            <div class="nav-buttons">
+                <button class="nav-btn active">
+                    <span class="nav-icon">💬</span>
+                </button>
+                <button class="nav-btn">
+                    <span class="nav-icon">📊</span>
+                </button>
+                <button class="nav-btn">
+                    <span class="nav-icon">🔄</span>
+                </button>
+            </div>
+        </div>
+
+        <!-- 主聊天区域 -->
+        <div class="chat-container">
+            <!-- 欢迎界面 -->
+            <div id="welcome-screen" class="welcome-screen">
+                <div class="welcome-content">
+                    <div class="welcome-icon">
+                        <span style="font-size: 64px;">🤖</span>
+                    </div>
+                    <h1 class="welcome-title">我是WMS智能助手,很高兴见到你!</h1>
+                    <p class="welcome-subtitle">我可以帮您处理订单加急标记、创建入库预约等任务,请把你的任务交给我~</p>
+
+                    <!-- 推荐功能 -->
+                    <div class="recommendations">
+                        <div class="recommendation-section">
+                            <h3>📦 订单加急</h3>
+                            <p>例如:"订单SOZ25090400035加急处理"</p>
+                        </div>
+                        <div class="recommendation-section">
+                            <h3>🚚 创建入库预约</h3>
+                            <p>例如:"梁涵9月20日上午送100箱货到九干仓,单号T2025072101115676"</p>
+                        </div>
+
+                        <div class="examples">
+                            <div class="example" onclick="sendExample('订单SOZ25090400035加急处理')">订单SOZ25090400035加急处理</div>
+                            <div class="example" onclick="sendExample('梁涵9月20日上午送100箱货到九干仓,单号T2025072101115676')">梁涵9月20日上午送100箱货到九干仓,单号T2025072101115676</div>
+                            <div class="example" onclick="sendExample('傲竹,订单BSIM2510100010004预计10月13日上午到货')">傲竹,订单BSIM2510100010004预计10月13日上午到货</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 聊天消息 -->
+            <div id="messages-container" class="messages-container" style="display: none;">
+                <div id="messages"></div>
+
+                <!-- 加载状态 -->
+                <div id="loading-message" class="message assistant" style="display: none;">
+                    <div class="message-content">
+                        <div class="assistant-message">
+                            <div class="assistant-icon">
+                                <span style="font-size: 16px;">🤖</span>
+                            </div>
+                            <div class="typing-indicator">
+                                <span class="dot"></span>
+                                <span class="dot"></span>
+                                <span class="dot"></span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 输入区域 -->
+            <div class="input-container">
+                <div class="input-box">
+                    <div class="input-wrapper">
+                        <input
+                            id="message-input"
+                            type="text"
+                            placeholder="给WMS智能助手发送消息"
+                            class="message-input"
+                            onkeypress="handleKeyPress(event)"
+                        />
+                        <div class="input-actions">
+                            <button class="action-btn" title="深度思考">
+                                🧠
+                            </button>
+                            <button class="action-btn" title="联网搜索">
+                                🌐
+                            </button>
+                            <button
+                                id="send-btn"
+                                class="send-btn"
+                                onclick="sendMessage()"
+                            >
+                                ↑
+                            </button>
+                        </div>
+                    </div>
+                </div>
+                <p class="disclaimer">内容由 AI 生成,请仔细核验</p>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        // 全局变量
+        const API_BASE_URL = 'http://127.0.0.1:8080';
+        let messages = [];
+        let isLoading = false;
+
+        // DOM元素
+        let welcomeScreen;
+        let messagesContainer;
+        let messagesDiv;
+        let messageInput;
+        let sendBtn;
+        let loadingMessage;
+        let currentAssistantElement = null;
+        let currentAssistantText = '';
+
+        // 初始化
+        document.addEventListener('DOMContentLoaded', function() {
+            welcomeScreen = document.getElementById('welcome-screen');
+            messagesContainer = document.getElementById('messages-container');
+            messagesDiv = document.getElementById('messages');
+            messageInput = document.getElementById('message-input');
+            sendBtn = document.getElementById('send-btn');
+            loadingMessage = document.getElementById('loading-message');
+
+            // 自动聚焦输入框
+            messageInput.focus();
+
+            console.log('聊天界面已初始化');
+        });
+
+        // 处理键盘事件
+        function handleKeyPress(event) {
+            if (event.key === 'Enter' && !event.shiftKey) {
+                event.preventDefault();
+                sendMessage();
+            }
+        }
+
+        // 发送示例消息
+        function sendExample(text) {
+            messageInput.value = text;
+            sendMessage();
+        }
+
+        // 发送消息
+        async function sendMessage() {
+            const message = messageInput.value.trim();
+            if (!message || isLoading) return;
+
+            // 隐藏欢迎界面,显示消息容器
+            if (messages.length === 0) {
+                welcomeScreen.style.display = 'none';
+                messagesContainer.style.display = 'block';
+            }
+
+            // 禁用输入和按钮
+            setInputState(false);
+
+            // 添加用户消息
+            addUserMessage(message);
+
+            // 显示加载状态
+            showLoading();
+
+            // 清空输入框
+            messageInput.value = '';
+
+            try {
+                const response = await fetch(`${API_BASE_URL}/api/agent/chat/stream`, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                    },
+                    body: JSON.stringify({
+                        message: message
+                    })
+                });
+
+                await handleStreamResponse(response);
+
+            } catch (error) {
+                console.error('请求失败:', error);
+
+                // 隐藏加载状态
+                hideLoading();
+
+                // 添加错误消息
+                addBotMessage('❌ 网络错误,请稍后重试。');
+            } finally {
+                // 重新启用输入和按钮
+                setInputState(true);
+                messageInput.focus();
+            }
+        }
+
+        // 添加用户消息
+        function addUserMessage(message) {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = 'message user';
+            messageDiv.innerHTML = `
+                <div class="message-content">
+                    <div class="user-message">
+                        ${escapeHtml(message)}
+                    </div>
+                </div>
+            `;
+
+            messagesDiv.appendChild(messageDiv);
+            messages.push({ type: 'user', content: message, timestamp: new Date() });
+            scrollToBottom();
+        }
+
+        // 添加机器人消息
+        function addBotMessage(message) {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = 'message assistant';
+            messageDiv.innerHTML = `
+                <div class="message-content">
+                    <div class="assistant-message">
+                        <div class="assistant-icon">
+                            <span style="font-size: 16px;">🤖</span>
+                        </div>
+
+                        <div class="assistant-text">${formatMessage(message)}</div>
+                    </div>
+                </div>
+            `;
+
+            messagesDiv.appendChild(messageDiv);
+            messages.push({ type: 'assistant', content: message, timestamp: new Date() });
+            scrollToBottom();
+        }
+
+        function createStreamingBotMessage() {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = 'message assistant';
+            messageDiv.innerHTML = `
+                <div class="message-content">
+                    <div class="assistant-message">
+                        <div class="assistant-icon">
+                            <span style="font-size: 16px;">🤖</span>
+                        </div>
+
+                        <div class="assistant-text"></div>
+                    </div>
+                </div>
+            `;
+
+            messagesDiv.appendChild(messageDiv);
+            messages.push({ type: 'assistant', content: '', timestamp: new Date() });
+            scrollToBottom();
+            currentAssistantElement = messageDiv.querySelector('.assistant-text');
+            currentAssistantText = '';
+        }
+
+        function updateStreamingBotMessage(text) {
+            if (!currentAssistantElement) {
+                createStreamingBotMessage();
+            }
+            currentAssistantText += text;
+            currentAssistantElement.innerHTML = formatMessage(currentAssistantText);
+            const lastMessage = messages[messages.length - 1];
+            if (lastMessage && lastMessage.type === 'assistant') {
+                lastMessage.content = currentAssistantText;
+            }
+            scrollToBottom();
+        }
+
+        async function handleStreamResponse(response) {
+            if (!response.ok) {
+                hideLoading();
+                addBotMessage('❌ 服务器返回错误,请稍后重试。');
+                return;
+            }
+
+            const reader = response.body?.getReader();
+            if (!reader) {
+                hideLoading();
+                addBotMessage('❌ 当前浏览器不支持流式响应。');
+                return;
+            }
+
+            createStreamingBotMessage();
+
+            const decoder = new TextDecoder('utf-8');
+            let buffer = '';
+            let streamFinished = false;
+
+            try {
+                while (!streamFinished) {
+                    const { value, done } = await reader.read();
+                    if (done) {
+                        break;
+                    }
+
+                    buffer += decoder.decode(value, { stream: true });
+                    let boundary = findSseBoundary(buffer);
+
+                    while (boundary.index !== -1) {
+                        const rawEvent = buffer.slice(0, boundary.index).replace(/\r/g, '').trim();
+                        buffer = buffer.slice(boundary.index + boundary.length);
+                        streamFinished = processSseEvent(rawEvent) || streamFinished;
+                        boundary = findSseBoundary(buffer);
+                    }
+                }
+
+                if (buffer.trim()) {
+                    streamFinished = processSseEvent(buffer.replace(/\r/g, '').trim()) || streamFinished;
+                }
+            } catch (error) {
+                console.error('处理流式响应失败:', error);
+                updateStreamingBotMessage('\n❌ 响应解析失败,请稍后重试。');
+            } finally {
+                hideLoading();
+                currentAssistantElement = null;
+                currentAssistantText = '';
+            }
+        }
+
+        function findSseBoundary(text) {
+            const lfIndex = text.indexOf('\n\n');
+            const crlfIndex = text.indexOf('\r\n\r\n');
+
+            if (lfIndex === -1 && crlfIndex === -1) {
+                return { index: -1, length: 0 };
+            }
+
+            if (lfIndex === -1) {
+                return { index: crlfIndex, length: 4 };
+            }
+
+            if (crlfIndex === -1) {
+                return { index: lfIndex, length: 2 };
+            }
+
+            return lfIndex < crlfIndex
+                ? { index: lfIndex, length: 2 }
+                : { index: crlfIndex, length: 4 };
+        }
+
+        function processSseEvent(rawEvent) {
+            if (!rawEvent) return false;
+
+            const lines = rawEvent.split('\n');
+            let finished = false;
+
+            for (const line of lines) {
+                const trimmedLine = line.trim();
+                if (!trimmedLine.startsWith('data:')) continue;
+                const dataStr = trimmedLine.slice(5).trim();
+                if (!dataStr) continue;
+
+                if (dataStr === '[DONE]') {
+                    finished = true;
+                    break;
+                }
+
+                let payload;
+                try {
+                    payload = JSON.parse(dataStr);
+                } catch (error) {
+                    console.warn('JSON解析失败,使用原始数据:', dataStr);
+                    payload = { type: 'text', text: dataStr };
+                }
+
+                if (payload.type === 'text' && payload.text) {
+                    updateStreamingBotMessage(payload.text);
+                } else if (payload.type === 'error') {
+                    updateStreamingBotMessage(`\n❌ ${payload.text || '发生未知错误'}`);
+                }
+            }
+
+            return finished;
+        }
+
+        // 显示加载状态
+        function showLoading() {
+            isLoading = true;
+            loadingMessage.style.display = 'block';
+            scrollToBottom();
+        }
+
+        // 隐藏加载状态
+        function hideLoading() {
+            isLoading = false;
+            loadingMessage.style.display = 'none';
+        }
+
+        // 设置输入状态
+        function setInputState(enabled) {
+            messageInput.disabled = !enabled;
+            sendBtn.disabled = !enabled;
+
+            if (enabled) {
+                sendBtn.innerHTML = '↑';
+            } else {
+                sendBtn.innerHTML = '<div class="loading"></div>';
+            }
+        }
+
+        // 滚动到底部
+        function scrollToBottom() {
+            messagesContainer.scrollTop = messagesContainer.scrollHeight;
+        }
+
+        // HTML转义
+        function escapeHtml(text) {
+            const div = document.createElement('div');
+            div.textContent = text;
+            return div.innerHTML;
+        }
+
+        // 格式化消息
+        function formatMessage(content) {
+            // 先进行HTML转义,再处理换行
+            return content.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
+        }
+    </script>
+
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+            background: #f8f9fa;
+        }
+
+        .warehouse-chat {
+            display: flex;
+            height: 100vh;
+            background: #f8f9fa;
+        }
+
+        .sidebar {
+            width: 60px;
+            background: #ffffff;
+            border-right: 1px solid #e5e7eb;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            padding: 16px 0;
+        }
+
+        .logo {
+            margin-bottom: 24px;
+        }
+
+        .logo-icon {
+            width: 32px;
+            height: 32px;
+            background: linear-gradient(135deg, #f8f8fc, #d6d1df);
+            border-radius: 8px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 16px;
+        }
+
+        .nav-buttons {
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+        }
+
+        .nav-btn {
+            width: 44px;
+            height: 44px;
+            border: none;
+            background: transparent;
+            border-radius: 8px;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: all 0.2s;
+        }
+
+        .nav-btn:hover {
+            background: #f3f4f6;
+        }
+
+        .nav-btn.active {
+            background: #eef2ff;
+            color: #4f46e5;
+        }
+
+        .nav-icon {
+            font-size: 18px;
+        }
+
+        .chat-container {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            max-width: 800px;
+            margin: 0 auto;
+            width: 100%;
+        }
+
+        .welcome-screen {
+            flex: 1;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 40px;
+        }
+
+        .welcome-content {
+            text-align: center;
+            max-width: 600px;
+        }
+
+        .welcome-icon {
+            font-size: 64px;
+            margin-bottom: 24px;
+        }
+
+        .welcome-title {
+            font-size: 28px;
+            font-weight: 600;
+            color: #1f2937;
+            margin-bottom: 16px;
+            line-height: 1.3;
+        }
+
+        .welcome-subtitle {
+            font-size: 16px;
+            color: #6b7280;
+            line-height: 1.5;
+            margin: 0 0 32px 0;
+        }
+
+        .recommendations {
+            text-align: left;
+            background: white;
+            border-radius: 16px;
+            padding: 24px;
+            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+            margin-top: 24px;
+        }
+
+        .recommendation-section {
+            margin-bottom: 20px;
+        }
+
+        .recommendation-section h3 {
+            font-size: 16px;
+            color: #1f2937;
+            margin-bottom: 8px;
+        }
+
+        .recommendation-section p {
+            font-size: 14px;
+            color: #6b7280;
+            margin: 0;
+        }
+
+        .examples {
+            display: flex;
+            flex-direction: column;
+            gap: 12px;
+            margin-top: 20px;
+        }
+
+        .example {
+            background: #f8f9fa;
+            border: 1px solid #e5e7eb;
+            border-radius: 12px;
+            padding: 12px 16px;
+            cursor: pointer;
+            transition: all 0.2s;
+            font-size: 14px;
+            color: #374151;
+        }
+
+        .example:hover {
+            background: #eef2ff;
+            border-color: #4f46e5;
+            color: #4f46e5;
+        }
+
+        .messages-container {
+            flex: 1;
+            overflow-y: auto;
+            padding: 24px;
+            padding-bottom: 0;
+        }
+
+        .message {
+            margin-bottom: 24px;
+        }
+
+        .message-content {
+            display: flex;
+            max-width: 100%;
+        }
+
+        .user-message {
+            background: #4f46e5;
+            color: white;
+            padding: 12px 18px;
+            border-radius: 18px;
+            margin-left: auto;
+            max-width: 70%;
+            word-wrap: break-word;
+            line-height: 1.4;
+        }
+
+        .assistant-message {
+            display: flex;
+            align-items: flex-start;
+            gap: 12px;
+            max-width: 80%;
+        }
+
+        .assistant-icon {
+            width: 32px;
+            height: 32px;
+            background: linear-gradient(135deg, #f5f5f7, #cfd5e1);
+            border-radius: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 14px;
+            flex-shrink: 0;
+        }
+
+        .assistant-text {
+            background: white;
+            border: 1px solid #e5e7eb;
+            padding: 12px 18px;
+            border-radius: 18px;
+            line-height: 1.5;
+            white-space: pre-line;
+            color: #1f2937;
+        }
+
+        .typing-indicator {
+            display: flex;
+            gap: 4px;
+            padding: 12px 18px;
+            background: white;
+            border: 1px solid #e5e7eb;
+            border-radius: 18px;
+        }
+
+        .dot {
+            width: 6px;
+            height: 6px;
+            background: #9ca3af;
+            border-radius: 50%;
+            animation: typing 1.4s infinite;
+        }
+
+        .dot:nth-child(2) {
+            animation-delay: 0.2s;
+        }
+
+        .dot:nth-child(3) {
+            animation-delay: 0.4s;
+        }
+
+        @keyframes typing {
+            0%, 60%, 100% {
+                transform: translateY(0);
+            }
+            30% {
+                transform: translateY(-10px);
+            }
+        }
+
+        .input-container {
+            padding: 24px;
+            background: white;
+            border-top: 1px solid #e5e7eb;
+        }
+
+        .input-box {
+            max-width: 100%;
+        }
+
+        .input-wrapper {
+            position: relative;
+            background: white;
+            border: 2px solid #e5e7eb;
+            border-radius: 24px;
+            padding: 8px 16px;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            transition: border-color 0.2s;
+        }
+
+        .input-wrapper:focus-within {
+            border-color: #4f46e5;
+        }
+
+        .message-input {
+            flex: 1;
+            border: none;
+            outline: none;
+            font-size: 16px;
+            padding: 8px 0;
+            background: transparent;
+            color: #1f2937;
+        }
+
+        .message-input::placeholder {
+            color: #9ca3af;
+        }
+
+        .input-actions {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+        }
+
+        .action-btn {
+            width: 32px;
+            height: 32px;
+            border: none;
+            background: transparent;
+            border-radius: 6px;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 14px;
+            transition: background 0.2s;
+        }
+
+        .action-btn:hover {
+            background: #f3f4f6;
+        }
+
+        .send-btn {
+            width: 32px;
+            height: 32px;
+            background: #4f46e5;
+            border: none;
+            border-radius: 50%;
+            color: white;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 16px;
+            font-weight: bold;
+            transition: all 0.2s;
+        }
+
+        .send-btn:hover:not(:disabled) {
+            background: #4338ca;
+            transform: translateY(-1px);
+        }
+
+        .send-btn:disabled {
+            background: #d1d5db;
+            cursor: not-allowed;
+        }
+
+        .disclaimer {
+            text-align: center;
+            font-size: 12px;
+            color: #9ca3af;
+            margin-top: 12px;
+            margin-bottom: 0;
+        }
+
+        /* 响应式设计 */
+        @media (max-width: 768px) {
+            .sidebar {
+                width: 50px;
+                padding: 12px 0;
+            }
+
+            .welcome-title {
+                font-size: 24px;
+            }
+
+            .messages-container {
+                padding: 16px;
+            }
+
+            .input-container {
+                padding: 16px;
+            }
+
+            .user-message,
+            .assistant-message {
+                max-width: 90%;
+            }
+
+            .examples {
+                gap: 8px;
+            }
+
+            .example {
+                padding: 10px 12px;
+                font-size: 13px;
+            }
+        }
+    </style>
+</body>
+</html>