From fe65f23fa705e00e32e176be16e58a6c2433ecb8 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Mon, 13 Apr 2026 23:31:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BD=91=E9=A1=B5=E7=AB=AF=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持图片上传(预览显示) - 支持文本文件上传(txt, md, json, csv等) - 支持 PDF 和 Word 文档 - 文件内容自动添加到消息中供 AI 分析 - 多文件同时上传支持 --- main_v2.py | 16 ++++- templates/index.html | 159 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 7 deletions(-) diff --git a/main_v2.py b/main_v2.py index 78d3ee2..27adc6a 100644 --- a/main_v2.py +++ b/main_v2.py @@ -766,6 +766,7 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str): elif action == "chat": message = data.get("message", "") + files = data.get("files", []) # 上传的文件 conversation_id = data.get("conversation_id") enable_thinking = data.get("enable_thinking", True) agent_id_override = data.get("agent_id") @@ -776,9 +777,22 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str): if agent and agent.is_active: current_agent_id = agent_id_override - if not message.strip(): + # 如果没有消息但有文件,构造消息 + if not message.strip() and files: + message = "[上传文件]" + + if not message.strip() and not files: continue + # 处理文件内容,添加到消息 + if files: + for f in files: + if f.get('type') and f['type'].startswith('image/'): + message += f"\n[图片: {f['name']}]" + elif f.get('content'): + # 文本文件内容 + message += f"\n\n文件 {f['name']} 内容:\n{f['content'][:3000]}" + # 1. 获取Agent配置 agent_config = agent_service.get_agent_config(current_agent_id) agent_tools = agent_config.get('agent', {}).get('tools', []) diff --git a/templates/index.html b/templates/index.html index aa1bb90..5c8e018 100644 --- a/templates/index.html +++ b/templates/index.html @@ -107,10 +107,23 @@ .input-row { display: flex; gap: 12px; align-items: center; } .input-row textarea { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 12px; font-size: 16px; resize: none; outline: none; max-height: 200px; min-height: 48px; line-height: 1.5; } .input-row textarea:focus { border-color: #10a37f; } + .upload-btn { width: 48px; height: 48px; border-radius: 12px; background: #f5f5f5; border: 1px solid #ddd; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; color: #666; } + .upload-btn:hover { background: #e8e8e8; border-color: #10a37f; color: #10a37f; } .send-btn { width: 48px; height: 48px; border-radius: 12px; background: #10a37f; border: none; color: #fff; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .send-btn:hover { background: #0d8c6d; } .send-btn:disabled { background: #ccc; cursor: not-allowed; } + /* 文件预览 */ + .file-preview-area { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; } + .file-preview-item { position: relative; border: 1px solid #e0e0e0; border-radius: 8px; padding: 8px; background: #f8f9fa; max-width: 200px; } + .file-preview-item.image-preview { padding: 4px; } + .file-preview-item img { max-width: 180px; max-height: 120px; border-radius: 6px; } + .file-preview-item .file-icon { display: flex; align-items: center; gap: 8px; } + .file-preview-item .file-icon i { font-size: 24px; color: #10a37f; } + .file-preview-item .file-name { font-size: 12px; color: #666; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .file-preview-item .file-remove { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border-radius: 50%; background: #ff4757; color: white; border: none; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; } + .file-preview-item .file-remove:hover { background: #ff6b7a; } + /* 快捷语句 - 横向扁平 */ .quick-phrases-bar { display: flex; align-items: center; gap: 8px; margin-top: 12px; position: relative; } .add-phrase-btn { padding: 6px 10px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; font-size: 12px; color: #666; white-space: nowrap; flex-shrink: 0; } @@ -166,9 +179,12 @@
+ +
+
@@ -773,32 +789,163 @@ function sendMessage() { const input = document.getElementById('messageInput'); const msg = input.value.trim(); - if (!msg) return; + + // 如果没有消息且没有文件,不发送 + if (!msg && pendingFiles.length === 0) return; + document.getElementById('sendBtn').disabled = true; input.value = ''; input.style.height = 'auto'; - // 立即显示用户消息(不等后端广播) - lastSentMessage = msg; // 记录最后发送的消息,避免重复显示 - appendMessage('user', msg); + // 立即显示用户消息(包含文件) + lastSentMessage = msg; + appendMessageWithFiles('user', msg, pendingFiles); + + // 清空文件预览 + pendingFiles = []; + document.getElementById('filePreviewArea').innerHTML = ''; // 获取工具禁用状态 const enableSearch = document.getElementById('enableSearch').checked; const disabledTools = []; if (!enableSearch) disabledTools.push('search'); + // 发送消息(包含文件) if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'chat', message: msg, conversation_id: currentConversationId, agent_id: currentAgentId, - disabled_tools: disabledTools // 禁用的工具列表 + disabled_tools: disabledTools, + files: lastSentFiles || [] // 发送的文件列表 })); } + + lastSentFiles = null; // 清空 } - let lastSentMessage = null; // 记录最后发送的消息 + let lastSentFiles = null; // 记录发送的文件 + + // 显示带文件的用户消息 + function appendMessageWithFiles(role, content, files) { + const container = document.getElementById('messagesContainer'); + container.querySelector('.welcome')?.remove(); + + const div = document.createElement('div'); + div.className = `message ${role}`; + + // 构建消息内容 + let msgContent = content; + if (files && files.length > 0) { + lastSentFiles = files.map(f => ({ + name: f.name, + type: f.type, + content: f.content + })); + + // 添加文件信息到消息 + let filesText = '\n\n[附件]'; + for (const f of files) { + if (f.type.startsWith('image/')) { + filesText += `\n图片: ${f.name}`; + } else { + filesText += `\n文件: ${f.name}`; + // 文本文件显示内容摘要 + if (!f.type.startsWith('image/') && f.content && f.content.length < 2000) { + filesText += `\n内容: ${f.content.substring(0, 500)}${f.content.length > 500 ? '...' : ''}`; + } + } + } + msgContent += filesText; + } + + const avatar = role === 'user' ? '👤' : '🤖'; + let html = `
${avatar}
`; + html += `
${escapeHtml(msgContent)}
`; + html += ``; + html += `
`; + html += '
'; + div.innerHTML = html; + container.appendChild(div); + + container.scrollTop = container.scrollHeight; + } + let pendingFiles = []; // 待发送的文件 + + // 文件上传处理 + function handleFileUpload(event) { + const files = event.target.files; + const previewArea = document.getElementById('filePreviewArea'); + + for (const file of files) { + const fileId = 'file-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); + + // 读取文件内容 + const reader = new FileReader(); + reader.onload = (e) => { + const fileData = { + id: fileId, + name: file.name, + type: file.type, + size: file.size, + content: e.target.result + }; + pendingFiles.push(fileData); + + // 显示预览 + const previewItem = document.createElement('div'); + previewItem.className = 'file-preview-item'; + previewItem.id = fileId + '-preview'; + + if (file.type.startsWith('image/')) { + previewItem.classList.add('image-preview'); + previewItem.innerHTML = ` + ${file.name} + + `; + } else { + // 文件图标 + let iconClass = 'ri-file-text-line'; + if (file.name.endsWith('.pdf')) iconClass = 'ri-file-pdf-line'; + else if (file.name.endsWith('.json')) iconClass = 'ri-code-line'; + else if (file.name.endsWith('.csv')) iconClass = 'ri-table-line'; + else if (file.type.includes('word') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) iconClass = 'ri-file-word-line'; + + previewItem.innerHTML = ` +
+ + ${file.name} +
+ + `; + } + + previewArea.appendChild(previewItem); + }; + + // 根据文件类型选择读取方式 + if (file.type.startsWith('image/')) { + reader.readAsDataURL(file); + } else if (file.type === 'application/pdf') { + reader.readAsDataURL(file); + } else { + // 文本类文件直接读取内容 + reader.readAsText(file); + } + } + + // 清空 input 以便再次选择 + event.target.value = ''; + } + + function removeFile(fileId) { + pendingFiles = pendingFiles.filter(f => f.id !== fileId); + const preview = document.getElementById(fileId + '-preview'); + if (preview) preview.remove(); + } + + // 发送消息 function setupTextarea() { const textarea = document.getElementById('messageInput');