Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
18 KiB
JSON
1 line
18 KiB
JSON
{"repo": "chiuhe007/room-management", "files": {"room-management-client/src/components/DeepSeekChat.vue": {"e79b40ffafa4bc890f5ddf8733dfa6ebbe8a0811": {"t": 1786067814.933373, "n": 16648, "h": "300817ab370a20ad", "c": "<template>\n <div class=\"deepseek-chat\">\n <!-- 工具栏:会话保存/加载/导出 -->\n <div class=\"chat-toolbar\">\n <div class=\"left\">\n <select v-model=\"currentConversationId\" @change=\"onSelectConversation\">\n <option value=\"\">新会话</option>\n <option v-for=\"conv in savedConversations\" :key=\"conv.id\" :value=\"conv.id\">{{ conv.title }} · {{ conv.savedAt }}</option>\n </select>\n <button class=\"tool-btn\" @click=\"saveConversation\" title=\"保存当前会话\">保存</button>\n <button class=\"tool-btn\" @click=\"exportConversation\" title=\"导出当前会话\">导出</button>\n <button class=\"tool-btn danger\" @click=\"clearConversation\" title=\"清空会话\">清空</button>\n </div>\n <div class=\"right\">\n <span class=\"status\">{{ autosaveMessage }}</span>\n </div>\n </div>\n\n <!-- 消息历史区域 -->\n <div class=\"message-history\" ref=\"messageContainer\">\n <div v-for=\"(message, index) in messages\" :key=\"message.id || index\" class=\"message\" :class=\"message.role\">\n <div class=\"avatar\" aria-hidden>\n <span v-if=\"message.role === 'assistant'\">🤖</span>\n <span v-else>🙋</span>\n </div>\n <div class=\"content\">\n <div class=\"role-line\">\n <div class=\"role-label\">{{ message.role === 'assistant' ? 'DeepSeek AI' : '您' }}</div>\n <div class=\"ts\">{{ formatShortDate(message.createdAt) }}</div>\n </div>\n\n <div v-if=\"message.thinking\" class=\"text thinking\">\n <span class=\"dot\" v-for=\"n in 3\" :key=\"n\"></span>\n </div>\n <div v-else class=\"text\" v-html=\"formatMessage(message.content)\"></div>\n </div>\n </div>\n </div>\n\n <!-- 输入区域 -->\n <div class=\"input-area\">\n <textarea v-model=\"userInput\" placeholder=\"输入您的问题...\" @keydown.enter.exact.prevent=\"sendMessage\"></textarea>\n <button @click=\"sendMessage\" :disabled=\"isLoading\">\n <span v-if=\"!isLoading\">发送</span>\n <span v-else class=\"loading-dots\">\n <span class=\"dot\" v-for=\"n in 3\" :key=\"n\"></span>\n </span>\n </button>\n </div>\n\n </div>\n</template>\n\n<script>\nexport default {\n name: 'DeepSeekChat',\n props: {\n apiKey: {\n type: String,\n default: 'sk-f7c238c5a4d4441aa49691a93554b1e7'\n },\n model: {\n type: String,\n default: 'deepseek-chat' // 可以是 deepseek-chat 或 deepseek-coder\n },\n temperature: {\n type: Number,\n default: 0.7\n },\n maxTokens: {\n type: Number,\n default: 2048\n }\n },\n data() {\n return {\n messages: [],\n savedConversations: [],\n currentConversationId: '',\n autosaveMessage: '',\n userInput: '',\n isLoading: false,\n abortController: null,\n currentThinkingId: null,\n autosaveTimer: null\n }\n },\n mounted() {\n this.loadSavedConversations();\n // 恢复草稿\n try {\n const raw = localStorage.getItem('deepseek_draft');\n if (raw) {\n const draft = JSON.parse(raw);\n if (draft && draft.messages) this.messages = draft.messages;\n }\n } catch (e) {\n console.error('加载草稿失败', e);\n }\n },\n watch: {\n messages: {\n handler() {\n this.scheduleAutosave();\n },\n deep: true\n }\n },\n methods: {\n async sendMessage() {\n if (!this.userInput.trim() || this.isLoading) return;\n\n // 添加用户消息(带 id 与时间戳)\n const userMsg = { id: 'u_' + Date.now(), role: 'user', content: this.userInput, createdAt: new Date().toISOString() };\n this.messages.push(userMsg);\n\n // 添加AI思考状态(带 id,便于流式更新时定位)\n const thinkingId = 'a_' + Date.now() + '_' + Math.floor(Math.random() * 10000);\n this.currentThinkingId = thinkingId;\n this.messages.push({ id: thinkingId, role: 'assistant', content: '', thinking: true, createdAt: '' });\n\n this.userInput = '';\n this.isLoading = true;\n this.scrollToBottom();\n\n // 创建可中止的请求\n this.abortController = new AbortController();\n\n try {\n const response = await this.fetchDeepSeekResponse();\n await this.handleResponse(response, thinkingId);\n } catch (error) {\n if (error.name !== 'AbortError') {\n this.handleError(error);\n }\n } finally {\n this.isLoading = false;\n this.abortController = null;\n this.scheduleAutosave();\n }\n },\n async fetchDeepSeekResponse() {\n // 仅发送最近若干条消息以避免请求过长(例如最近 20 条)\n const history = this.messages.filter(m => !m.thinking).slice(-20).map(m => ({ role: m.role, content: m.content }));\n\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`\n },\n body: JSON.stringify({\n model: this.model,\n messages: history,\n temperature: this.temperature,\n max_tokens: this.maxTokens,\n stream: true\n }),\n signal: this.abortController.signal\n });\n\n if (!response.ok) {\n throw new Error(`API请求失败: ${response.status}`);\n }\n\n return response;\n },\n async handleResponse(response, thinkingId) {\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const chunk = decoder.decode(value, { stream: true });\n const lines = chunk.split('\\n').filter(line => line.trim() !== '');\n\n for (const line of lines) {\n if (line.startsWith('data:')) {\n const data = line.replace('data:', '').trim();\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const deltaContent = parsed.choices?.[0]?.delta?.content;\n if (deltaContent) {\n // 按 id 查找对应的 thinking 消息并追加\n const idx = this.messages.findIndex(m => m.id === thinkingId);\n if (idx !== -1) {\n this.messages[idx].content += deltaContent;\n this.scrollToBottom();\n } else {\n this.messages.push({ id: thinkingId, role: 'assistant', content: deltaContent, createdAt: new Date().toISOString() });\n }\n }\n } catch (e) {\n console.error('解析错误:', e);\n }\n }\n }\n }\n\n // 流结束后标记时间戳和 thinking=false\n const finalIdx = this.messages.findIndex(m => m.id === thinkingId);\n if (finalIdx !== -1) {\n this.messages[finalIdx].thinking = false;\n if (!this.messages[finalIdx].createdAt) this.messages[finalIdx].createdAt = new Date().toISOString();\n }\n this.currentThinkingId = null;\n },\n handleError(error) {\n console.error('DeepSeek API错误:', error);\n // 移除对应 thinking 状态消息(优先通过 id)\n const index = this.messages.findIndex(m => m.id === this.currentThinkingId || m.thinking);\n if (index !== -1) this.messages.splice(index, 1);\n this.messages.push({\n id: 'err_' + Date.now(),\n role: 'assistant',\n content: `抱歉,请求过程中出现错误: ${error.message}`,\n createdAt: new Date().toISOString()\n });\n this.currentThinkingId = null;\n },\n formatMessage(text) {\n return text\n .replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')\n .replace(/\\*(.*?)\\*/g, '<em>$1</em>')\n .replace(/`(.*?)`/g, '<code>$1</code>')\n .replace(/\\n/g, '<br>');\n },\n formatShortDate(iso) {\n if (!iso) return '-';\n const d = new Date(iso);\n if (isNaN(d)) return String(iso);\n return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n },\n scrollToBottom() {\n this.$nextTick(() => {\n const container = this.$refs.messageContainer;\n if (container) container.scrollTop = container.scrollHeight;\n });\n },\n // 本地保存/加载会话相关(轻量)\n loadSavedConversations() {\n try {\n const raw = localStorage.getItem('deepseek_conversations');\n this.savedConversations = raw ? JSON.parse(raw) : [];\n } catch (e) {\n console.error('读取本地会话失败', e);\n this.savedConversations = [];\n }\n },\n persistSavedConversations() {\n try {\n localStorage.setItem('deepseek_conversations', JSON.stringify(this.savedConversations));\n } catch (e) {\n console.error('保存会话失败', e);\n }\n },\n saveConversation() {\n const title = prompt('请输入会话标题(可选)', `会话 ${new Date().toLocaleString()}`) || `会话 ${new Date().toLocaleString()}`;\n const conv = {\n id: 'c_' + Date.now(),\n title,\n messages: JSON.parse(JSON.stringify(this.messages)),\n savedAt: new Date().toLocaleString()\n };\n this.savedConversations.unshift(conv);\n this.persistSavedConversations();\n this.currentConversationId = conv.id;\n this.autosaveMessage = '已保存';\n setTimeout(() => (this.autosaveMessage = ''), 1500);\n },\n exportConversation() {\n const data = { title: `会话_${new Date().toISOString()}`, messages: this.messages };\n const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `${data.title}.json`;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n },\n loadConversation(id) {\n if (!id) {\n this.messages = [];\n this.currentConversationId = '';\n return;\n }\n const conv = this.savedConversations.find(c => c.id === id);\n if (conv) {\n this.messages = JSON.parse(JSON.stringify(conv.messages));\n this.currentConversationId = id;\n }\n },\n onSelectConversation(e) {\n this.loadConversation(this.currentConversationId);\n },\n clearConversation() {\n if (confirm('清空当前会话?')) {\n this.messages = [];\n this.currentConversationId = '';\n }\n },\n // 自动保存当前会话为草稿(本地),节流实现\n scheduleAutosave() {\n if (this.autosaveTimer) clearTimeout(this.autosaveTimer);\n this.autosaveTimer = setTimeout(() => {\n try {\n localStorage.setItem('deepseek_draft', JSON.stringify({ messages: this.messages, updatedAt: new Date().toLocaleString() }));\n this.autosaveMessage = '已自动保存';\n setTimeout(() => (this.autosaveMessage = ''), 1200);\n } catch (e) {\n console.error('自动保存失败', e);\n }\n }, 2000);\n },\n abortRequest() {\n if (this.abortController) {\n this.abortController.abort();\n }\n }\n },\n beforeUnmount() {\n this.abortRequest();\n }\n};\n</script>\n\n<style scoped>\n.deepseek-chat {\n display: flex;\n flex-direction: column;\n height: 100%;\n max-width: 800px;\n margin: 0 auto;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n overflow: hidden;\n background-color: #f9fafb;\n}\n\n.message-history {\n flex: 1;\n padding: 16px;\n overflow-y: auto;\n max-height: 500px;\n}\n\n.message {\n display: flex;\n align-items: flex-start;\n margin-bottom: 12px;\n gap: 12px;\n}\n\n.message.assistant {\n flex-direction: row;\n}\n\n.message.user {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex-shrink: 0;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n overflow: hidden;\n background-color: #e6eefc;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n}\n\n.content {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n max-width: 72%;\n}\n\n.role-line {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 6px;\n}\n\n.role-label {\n font-size: 12px;\n color: #6b7280;\n}\n\n.ts {\n font-size: 11px;\n color: #9ca3af;\n}\n\n.text {\n padding: 10px 14px;\n border-radius: 10px;\n line-height: 1.5;\n word-break: break-word;\n box-shadow: 0 1px 0 rgba(16,24,40,0.03);\n}\n\n.assistant .text {\n background-color: #ffffff;\n border: 1px solid #e6edf8;\n color: #111827;\n}\n\n.user .text {\n background: linear-gradient(180deg,#2563eb,#1e40af);\n color: #ffffff;\n align-self: flex-end;\n}\n\n.text code {\n background-color: #f3f4f6;\n padding: 2px 6px;\n border-radius: 6px;\n}\n\ncode {\n font-family: monospace;\n background-color: #f3f4f6;\n padding: 2px 4px;\n border-radius: 4px;\n font-size: 0.9em;\n}\n\n.thinking,\n.loading-dots {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 8px 12px;\n}\n\n.dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background-color: #9ca3af;\n animation: bounce 1.4s infinite ease-in-out;\n}\n\n.dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n\n.dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n\n@keyframes bounce {\n\n 0%,\n 80%,\n 100% {\n transform: scale(0);\n }\n\n 40% {\n transform: scale(1);\n }\n}\n\n.input-area {\n display: flex;\n padding: 12px;\n border-top: 1px solid #e5e7eb;\n background-color: white;\n}\n\n.chat-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 12px;\n background: linear-gradient(180deg, #ffffff, #f7fbff);\n border-bottom: 1px solid #eef2ff;\n}\n\n.chat-toolbar .left {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.chat-toolbar select {\n padding: 6px 8px;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n background: white;\n}\n\n.tool-btn {\n padding: 6px 10px;\n border-radius: 6px;\n border: 1px solid transparent;\n background: #f3f4f6;\n cursor: pointer;\n font-size: 13px;\n}\n\n.tool-btn:hover { background: #eef2ff }\n.tool-btn.danger { background: #fee2e2; border-color: #fecaca }\n\n.chat-toolbar .right { font-size: 13px; color: #6b7280 }\n\ntextarea {\n flex: 1;\n padding: 10px 12px;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n resize: none;\n min-height: 44px;\n max-height: 120px;\n outline: none;\n font-family: inherit;\n font-size: 14px;\n transition: border-color 0.2s;\n}\n\ntextarea:focus {\n border-color: #3b82f6;\n}\n\nbutton {\n margin-left: 8px;\n padding: 0 16px;\n background-color: #3b82f6;\n color: white;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n font-weight: 500;\n transition: background-color 0.2s;\n min-width: 80px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\nbutton:hover {\n background-color: #2563eb;\n}\n\nbutton:disabled {\n background-color: #9ca3af;\n cursor: not-allowed;\n}\n</style>\n"}}}} |