# Chat Enhancement Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为沟通系统添加文件上传(AI理解内容)和表情包选择器功能 **Architecture:** 前端在 ChatView 输入区添加附件/Emoji按钮,后端扩展 ChatRequest 支持 file_ids,AgentService 读取文件内容作为上下文 **Tech Stack:** Vue 3 + TypeScript + FastAPI + SQLAlchemy + ChromaDB --- ## File Structure ``` frontend/src/ ├── views/ │ └── ChatView.vue # 修改 - 添加附件/Emoji按钮 ├── components/ │ └── chat/ │ ├── EmojiPicker.vue # 新建 - Emoji选择器 │ └── FileMessage.vue # 新建 - 文件消息气泡 └── api/ ├── conversation.ts # 修改 - chat支持file_ids └── document.ts # 新增 - getDocumentContent backend/app/ ├── routers/ │ ├── conversation.py # 修改 - ChatRequest支持file_ids │ └── document.py # 修改 - 新增content接口 ├── services/ │ └── agent_service.py # 修改 - chat支持文件上下文 └── models/ └── conversation.py # 修改 - Message新增attachments字段 ``` --- ## Task 1: 创建 EmojiPicker 组件 **Files:** - Create: `frontend/src/components/chat/EmojiPicker.vue` - [ ] **Step 1: 创建 EmojiPicker.vue** ```vue ``` --- ## Task 2: 创建 FileMessage 组件 **Files:** - Create: `frontend/src/components/chat/FileMessage.vue` - [ ] **Step 1: 创建 FileMessage.vue** ```vue ``` --- ## Task 3: 修改前端 API - conversation.ts **Files:** - Modify: `frontend/src/api/conversation.ts` - [ ] **Step 1: 修改 conversation.ts** ```typescript // 在 chat 方法中添加 file_ids 参数 chat(message: string, conversationId?: string, fileIds: string[] = []) { return api.post('/api/conversations/chat', { message, conversation_id: conversationId, file_ids: fileIds, }) } ``` --- ## Task 4: 修改前端 API - document.ts **Files:** - Modify: `frontend/src/api/document.ts` - [ ] **Step 1: 添加 getContent 方法** ```typescript // 新增方法 getContent(id: string) { return api.get(`/api/documents/${id}/content`) } ``` --- ## Task 5: 修改 ChatView.vue - 添加按钮和状态 **Files:** - Modify: `frontend/src/views/ChatView.vue` - [ ] **Step 1: 在 script setup 中添加以下内容** 在 import 后添加: ```typescript import EmojiPicker from '@/components/chat/EmojiPicker.vue' import FileMessage from '@/components/chat/FileMessage.vue' import { Paperclip, Smile, Download } from 'lucide-vue-next' // 新增状态 const fileInputRef = ref() const showEmojiPicker = ref(false) const selectedFiles = ref<{ id: string; name: string; type: string; size: number }[]>([]) const uploadingFiles = ref<{ name: string; progress: number }[]>([]) ``` - [ ] **Step 2: 添加文件上传方法** ```typescript async function handleFileSelect(e: Event) { const input = e.target as HTMLInputElement if (!input.files?.length) return for (const file of input.files) { // 校验大小 if (file.size > 10 * 1024 * 1024) { alert(`文件 ${file.name} 超过10MB限制`) continue } // 显示上传中状态 uploadingFiles.value.push({ name: file.name, progress: 0 }) try { const response = await documentApi.upload(file) selectedFiles.value.push({ id: response.data.id, name: file.name, type: file.type, size: file.size, }) } catch (e) { console.error('上传失败:', e) alert(`文件 ${file.name} 上传失败`) } finally { uploadingFiles.value = uploadingFiles.value.filter(f => f.name !== file.name) } } // 清空 input if (fileInputRef.value) { fileInputRef.value.value = '' } } function insertEmoji(emoji: string) { inputMessage.value += emoji showEmojiPicker.value = false } function openFilePicker() { fileInputRef.value?.click() } ``` - [ ] **Step 3: 在 sendMessage 中处理文件上传** 修改 sendMessage 函数,在发送消息时附带 file_ids: ```typescript async function sendMessage() { if (!inputMessage.value.trim() || isSending.value) return // 如果有文件,先上传 if (selectedFiles.value.length > 0) { // file_ids 已经在 selectedFiles 中 } isSending.value = true isTyping.value = true const text = inputMessage.value.trim() const fileIds = selectedFiles.value.map(f => f.id) // 添加用户消息(带文件) store.addMessage({ id: `temp-${Date.now()}`, role: 'user', content: text, created_at: new Date().toISOString(), attachments: selectedFiles.value, }) inputMessage.value = '' selectedFiles.value = [] // 清空已选文件 // ... 后续发送逻辑,传入 fileIds } ``` - [ ] **Step 4: 在 template 中添加附件和 Emoji 按钮** 在输入框的按钮区域添加: ```vue
``` - [ ] **Step 5: 添加样式** 在 style 部分添加: ```css /* 文件消息样式 */ .file-msg-row { display: flex; align-items: flex-start; gap: 12px; padding: 8px 0; } .file-msg-row .msg-avatar { margin-top: 4px; } /* 附件按钮 */ .attach-btn { width: 36px; height: 36px; border-radius: var(--radius-md); background: transparent; border: 1px solid transparent; color: var(--text-dim); display: flex; align-items: center; justify-content: center; transition: all var(--transition-fast); } .attach-btn:hover { background: var(--accent-cyan-dim); border-color: var(--border-mid); color: var(--accent-cyan); } /* Emoji 按钮 */ .emoji-wrapper { position: relative; } .emoji-btn { width: 36px; height: 36px; border-radius: var(--radius-md); background: transparent; border: 1px solid transparent; color: var(--text-dim); display: flex; align-items: center; justify-content: center; transition: all var(--transition-fast); } .emoji-btn:hover, .emoji-btn.active { background: var(--accent-cyan-dim); border-color: var(--border-mid); color: var(--accent-cyan); } ``` --- ## Task 6: 修改后端 - ChatRequest 支持 file_ids **Files:** - Modify: `backend/app/schemas/conversation.py` - [ ] **Step 1: 修改 ChatRequest** ```python class ChatRequest(BaseModel): message: str conversation_id: str | None = None agent_id: str | None = None file_ids: list[str] = [] # 新增 ``` --- ## Task 7: 修改后端 - Message 新增 attachments 字段 **Files:** - Modify: `backend/app/models/conversation.py` - [ ] **Step 1: 修改 Message 模型** ```python class Message(BaseModel): __tablename__ = "messages" conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=False, index=True) role = Column(String(20), nullable=False) # user, assistant, system content = Column(Text, nullable=False) model = Column(String(100), nullable=True) tokens_used = Column(Integer, nullable=True) attachments = Column(JSON, nullable=True) # 新增: [{file_id, filename, file_type, file_size}] conversation = relationship("Conversation", back_populates="messages") ``` --- ## Task 8: 修改后端 - 新增 document content 接口 **Files:** - Modify: `backend/app/routers/document.py` - [ ] **Step 1: 添加 content 接口** ```python @router.get("/{document_id}/content") async def get_document_content( document_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取文档的文本内容(用于AI理解)""" from app.services.document_service import DocumentService doc_svc = DocumentService(db) content = await doc_svc.get_document_content(current_user.id, document_id) if content is None: raise HTTPException(status_code=404, detail="文档不存在或无内容") return {"content": content} ``` --- ## Task 9: 修改后端 - DocumentService 新增 get_document_content **Files:** - Modify: `backend/app/services/document_service.py` - [ ] **Step 1: 添加 get_document_content 方法** ```python async def get_document_content(self, user_id: str, document_id: str) -> str | None: """获取文档的文本内容""" import os result = await self.db.execute( select(Document).where( Document.id == document_id, Document.user_id == user_id, ) ) doc = result.scalar_one_or_none() if not doc: return None file_path = doc.file_path if not os.path.exists(file_path): return None # 根据文件类型读取内容 ext = doc.filename.split('.')[-1].lower() try: if ext == 'txt': with open(file_path, 'r', encoding='utf-8') as f: return f.read() elif ext == 'md': with open(file_path, 'r', encoding='utf-8') as f: return f.read() elif ext == 'pdf': # 简单文本提取(生产环境应使用专业库) # 这里可以先用 pdfplumber 或 PyPDF2 return f"[PDF文档] {doc.filename}" else: return f"[文档] {doc.filename}" except Exception: return f"[文档] {doc.filename}" ``` --- ## Task 10: 修改后端 - AgentService 支持文件上下文 **Files:** - Modify: `backend/app/services/agent_service.py` - [ ] **Step 1: 修改 chat_simple 方法支持 file_ids** 在 chat_simple 方法中: ```python async def chat_simple( self, user_id: str, message: str, conversation_id: str | None = None, file_ids: list[str] = None, ) -> tuple[str, str, str]: # ... 现有逻辑 ... # 如果有文件,读取内容作为上下文 file_context = "" if file_ids: from app.services.document_service import DocumentService doc_svc = DocumentService(self.db) for file_id in file_ids: content = await doc_svc.get_document_content(user_id, file_id) if content: file_context += f"\n\n[用户上传文件内容]\n{content}\n[/文件内容]" # 将文件上下文添加到消息 full_message = f"{message}\n{file_context}" if file_context else message # 调用 LLM response = await self.llm.chat(full_message, ...) ``` --- ## Task 11: 验证和测试 - [ ] **Step 1: 前端 TypeScript 检查** ```bash cd frontend && npx vue-tsc --noEmit ``` - [ ] **Step 2: 后端语法检查** ```bash cd backend && python -m py_compile app/routers/conversation.py app/services/agent_service.py app/services/document_service.py ``` - [ ] **Step 3: 启动服务测试** ```bash # 后端 cd backend && python -m uvicorn app.main:app --reload # 前端 cd frontend && npm run dev ``` --- ## 执行选项 **1. Subagent-Driven (推荐)** - 我为每个任务派遣独立的子代理,任务间进行审查,快速迭代 **2. Inline Execution** - 在当前会话中按批次执行任务 选择哪种方式?