软件研发 --- SSE示例代码
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>API 测试页面 - 打字机效果</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Microsoft YaHei', Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); overflow: hidden; } .header { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 30px; text-align: center; } .header h1 { font-size: 2.5em; margin-bottom: 10px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .header p { font-size: 1.1em; opacity: 0.9; } .content { padding: 30px; } .input-section { margin-bottom: 30px; } .input-group { margin-bottom: 20px; } label { display: block; margin-bottom: 8px; font-weight: bold; color: #333; } input[type="text"], textarea { width: 100%; padding: 12px 15px; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 16px; transition: border-color 0.3s ease; } input[type="text"]:focus, textarea:focus { outline: none; border-color: #4facfe; box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.1); } textarea { resize: vertical; min-height: 100px; } .button-group { display: flex; gap: 15px; margin-bottom: 30px; } button { flex: 1; padding: 15px 25px; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .btn-primary { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(79, 172, 254, 0.3); } .btn-secondary { background: #f8f9fa; color: #6c757d; border: 2px solid #e9ecef; } .btn-secondary:hover { background: #e9ecef; color: #495057; } .btn-danger { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); color: white; } .btn-danger:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(255, 107, 107, 0.3); } .output-section { background: #f8f9fa; border-radius: 10px; padding: 25px; margin-top: 20px; } .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .output-title { font-size: 1.2em; font-weight: bold; color: #333; } .status-indicator { padding: 5px 12px; border-radius: 20px; font-size: 0.9em; font-weight: bold; } .status-waiting { background: #fff3cd; color: #856404; } .status-loading { background: #d1ecf1; color: #0c5460; } .status-complete { background: #d4edda; color: #155724; } .status-error { background: #f8d7da; color: #721c24; } .output-content { background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; min-height: 200px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; position: relative; } .typing-cursor { display: inline-block; width: 2px; height: 1.2em; background: #4facfe; animation: blink 1s infinite; margin-left: 2px; } @keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } } .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; } .stat-item { background: white; padding: 15px; border-radius: 8px; border-left: 4px solid #4facfe; } .stat-label { font-size: 0.9em; color: #6c757d; margin-bottom: 5px; } .stat-value { font-size: 1.1em; font-weight: bold; color: #333; } .loading-spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #4facfe; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error-message { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #dc3545; } .quick-test-btn { padding: 8px 15px; background: #e9ecef; border: 1px solid #ced4da; border-radius: 5px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; } .quick-test-btn:hover { background: #4facfe; color: white; border-color: #4facfe; } @media (max-width: 768px) { .container { margin: 10px; border-radius: 10px; } .content { padding: 20px; } .button-group { flex-direction: column; } .stats { grid-template-columns: 1fr; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🤖 API 测试工具</h1> <p>测试流式API响应,支持打字机效果和Unicode转中文</p> </div> <div class="content"> <div class="input-section"> <div class="input-group"> <label for="apiUrl">API 地址:</label> <input type="text" id="apiUrl" value="http://118.182.164.160:8006/agro/sndmx-api/agri-gpt/v2/service-api/qa/chat-messages/streaming"> </div> <div class="input-group"> <label for="queryInput">查询内容:</label> <textarea id="queryInput" placeholder="请输入您的查询内容...">北京猪心的价格</textarea> </div> <div class="input-group"> <label>快速测试选项:</label> <div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 8px;"> <button type="button" class="quick-test-btn" onclick="setQuery('vxcv')">测试查询1</button> <button type="button" class="quick-test-btn" onclick="setQuery('北京猪心的价格')">价格查询</button> <button type="button" class="quick-test-btn" onclick="setQuery('你好')">问候测试</button> <button type="button" class="quick-test-btn" onclick="setQuery('你是谁')">身份询问</button> </div> </div> </div> <div class="button-group"> <button class="btn-primary" onclick="sendRequest()"> <span id="sendButtonText">🚀 发送请求</span> </button> <button class="btn-secondary" onclick="clearOutput()">🗑️ 清空输出</button> <button class="btn-danger" onclick="stopRequest()">⏹️ 停止请求</button> </div> <div class="output-section"> <div class="output-header"> <div class="output-title">📝 响应输出</div> <div id="statusIndicator" class="status-indicator status-waiting">等待中</div> </div> <div id="outputContent" class="output-content"> 点击"发送请求"开始测试... </div> <div class="stats"> <div class="stat-item"> <div class="stat-label">响应时间</div> <div id="responseTime" class="stat-value">0ms</div> </div> <div class="stat-item"> <div class="stat-label">字符数量</div> <div id="charCount" class="stat-value">0</div> </div> <div class="stat-item"> <div class="stat-label">消息数量</div> <div id="messageCount" class="stat-value">0</div> </div> <div class="stat-item"> <div class="stat-label">状态</div> <div id="requestStatus" class="stat-value">就绪</div> </div> </div> </div> </div> </div> <script> let currentRequest = null; let startTime = 0; let totalChars = 0; let messageCount = 0; let typingTimeout = null; // Unicode转中文函数 function unicodeToChinese(str) { return str.replace(/\\u[\dA-Fa-f]{4}/g, function (match) { return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)); }); } // 更新状态指示器 function updateStatus(status, text) { const indicator = document.getElementById('statusIndicator'); const statusElement = document.getElementById('requestStatus'); indicator.className = `status-indicator status-${status}`; indicator.textContent = text; statusElement.textContent = text; } // 更新统计信息 function updateStats() { document.getElementById('responseTime').textContent = startTime ? (Date.now() - startTime) + 'ms' : '0ms'; document.getElementById('charCount').textContent = totalChars; document.getElementById('messageCount').textContent = messageCount; } // 打字机效果输出 function typewriterEffect(element, text, speed = 50) { return new Promise((resolve) => { let i = 0; const cursor = document.createElement('span'); cursor.className = 'typing-cursor'; element.appendChild(cursor); function type() { if (i < text.length) { element.insertBefore(document.createTextNode(text.charAt(i)), cursor); i++; totalChars++; updateStats(); typingTimeout = setTimeout(type, speed); } else { cursor.remove(); resolve(); } } type(); }); } // 发送请求 async function sendRequest() { const apiUrl = document.getElementById('apiUrl').value.trim(); const query = document.getElementById('queryInput').value.trim(); const outputElement = document.getElementById('outputContent'); const sendButton = document.getElementById('sendButtonText'); if (!apiUrl || !query) { alert('请填写API地址和查询内容!'); return; } // 重置状态 totalChars = 0; messageCount = 0; startTime = Date.now(); outputElement.innerHTML = ''; updateStatus('loading', '请求中'); sendButton.innerHTML = '<span class="loading-spinner"></span>发送中...'; try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: query }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } currentRequest = response; const reader = response.body.getReader(); const decoder = new TextDecoder(); updateStatus('loading', '接收数据中'); // 添加调试信息显示区域 const debugElement = document.createElement('div'); debugElement.style.cssText = 'background:#f8f9fa;border:1px solid #dee2e6;padding:10px;margin-bottom:15px;font-size:12px;max-height:200px;overflow-y:auto;'; debugElement.innerHTML = '<strong>📡 调试信息:</strong><br>'; outputElement.appendChild(debugElement); const answerElement = document.createElement('div'); answerElement.style.cssText = 'background:white;border:1px solid #dee2e6;padding:15px;border-radius:5px;'; answerElement.innerHTML = '<strong>💬 AI回答:</strong><br>'; outputElement.appendChild(answerElement); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { updateStatus('complete', '完成'); break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // 保留最后一个可能不完整的行 for (const line of lines) { if (line.trim() && line.startsWith('data:')) { try { const jsonStr = line.substring(5); // 移除 "data:" 前缀 const data = JSON.parse(jsonStr); // 显示调试信息 const eventInfo = `[${data.event}] ${data.data?.node_type || ''} ${data.data?.title || ''}`; debugElement.innerHTML += `<span style="color:#666;">${eventInfo}</span><br>`; debugElement.scrollTop = debugElement.scrollHeight; // 处理answer字段 if (data.answer !== undefined) { messageCount++; // 转换Unicode为中文 const chineseText = unicodeToChinese(data.answer); // 使用打字机效果输出 await typewriterEffect(answerElement, chineseText, 30); } // 处理工具调用结果 if (data.event === 'node_finished' && data.data?.node_type === 'tool') { const toolResult = data.data.outputs; if (toolResult) { debugElement.innerHTML += `<span style="color:#28a745;">🔧 工具调用完成</span><br>`; } } } catch (e) { console.warn('解析JSON失败:', line, e); debugElement.innerHTML += `<span style="color:#dc3545;">❌ JSON解析错误</span><br>`; } } } } } catch (error) { updateStatus('error', '错误'); outputElement.innerHTML = `<div class="error-message"> <strong>请求失败:</strong><br> ${error.message}<br><br> <strong>可能的原因:</strong><br> • 网络连接问题<br> • API地址不正确<br> • 服务器暂时不可用<br> • 跨域请求被阻止<br> • 请尝试使用代理或CORS扩展 </div>`; } finally { sendButton.textContent = '🚀 发送请求'; currentRequest = null; updateStats(); } } // 停止请求 function stopRequest() { if (currentRequest) { currentRequest = null; updateStatus('error', '已停止'); document.getElementById('sendButtonText').textContent = '🚀 发送请求'; } if (typingTimeout) { clearTimeout(typingTimeout); typingTimeout = null; } } // 清空输出 function clearOutput() { document.getElementById('outputContent').innerHTML = '点击"发送请求"开始测试...'; totalChars = 0; messageCount = 0; startTime = 0; updateStatus('waiting', '等待中'); updateStats(); } // 设置查询内容 function setQuery(query) { document.getElementById('queryInput').value = query; } // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', function() { updateStats(); // 添加回车键发送功能 document.getElementById('queryInput').addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === 'Enter') { sendRequest(); } }); }); </script> </body> </html>