Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
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.
2026-08-07 12:53:47 +08:00

1 line
18 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}