// ==UserScript== // @name DeepSeek 消息真实时间戳 // @namespace https://github.com/miaomiaolu966 // @version 0.7.2 // @description 给 DeepSeek 每条消息加真实发送时间(微信风格)。直接读浏览器本地 IndexedDB 拿逐条 inserted_at,不再回退成"现在"。 // @author 喵喵露, WorkBuddy // @icon https://www.deepseek.com/favicon.ico // @match https://chat.deepseek.com/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ================== 配置 ================== const CFG = { rowSelector: '[data-virtual-list-item-key]', stampClass: 'ds-ts', debug: false, DB_NAME: 'deepseek-chat', // 来自 DeepSeek 网页源码:indexedDB.open("deepseek-chat",1) DB_VERSION: 1, STORE: 'history-message', // object store 名称 netFallback: true, // IDB 取不到时,回退到独立完整 history_messages 请求 }; // ========================================== // 先解释给用户:这里的"客户端"就是你的浏览器。DeepSeek 网页把历史对话 // 存进浏览器自带的 IndexedDB 本地数据库(库名 deepseek-chat),所以你重开 // 对话时根本不用重新下载就能显示历史——真实时间就安静躺在那个本地库里。 const style = document.createElement('style'); style.textContent = ` .${CFG.stampClass} { font-size: 11px; color: #9aa0a6; text-align: center; margin: 6px 4px 2px; line-height: 1.4; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; user-select: text; pointer-events: auto; } .${CFG.stampClass}.bot { display: inline-flex; align-items: center; margin: 0 8px 0 2px; text-align: left; } .${CFG.stampClass}.pending { color: #5f6368; opacity: .55; } `; const log = (...a) => { if (CFG.debug) console.log('[DS时间戳]', ...a); }; const info = (...a) => console.info('[DS时间戳]', ...a); // ---------- 时间格式化(微信风格) ---------- function fmt(d) { const p = (n) => String(n).padStart(2, '0'); const now = new Date(); const hm = `${p(d.getHours())}:${p(d.getMinutes())}`; const same = (x) => d.getFullYear() === x.getFullYear() && d.getMonth() === x.getMonth() && d.getDate() === x.getDate(); if (same(now)) return hm; const y = new Date(now); y.setDate(now.getDate() - 1); if (same(y)) return `昨天 ${hm}`; if (d.getFullYear() === now.getFullYear()) return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${hm}`; return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${hm}`; } function toMs(v) { if (typeof v === 'number' && isFinite(v)) return v > 1e12 ? v : v * 1000; if (typeof v === 'string' && /^\d+(\.\d+)?$/.test(v.trim())) { const n = Number(v.trim()); return n > 1e12 ? n : n * 1000; } return 0; } // ---------- 状态 ---------- const MSG_MAP = new Map(); // 消息id(Number) -> {date:Date, role:string} let curSession = null; let loaded = false; let busy = false; let refreshTimer = null; let dbPromise = null; // ---------- 会话 id ---------- function sessionOf() { const m = location.pathname.match(/\/a\/chat\/s\/([^/?#]+)/i); return m ? m[1] : null; } // ---------- IndexedDB 读取(主路径) ---------- function openDB() { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { try { const req = indexedDB.open(CFG.DB_NAME, CFG.DB_VERSION); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); // 只读打开,绝不触发 upgradeneeded,避免误改库结构 } catch (e) { reject(e); } }); return dbPromise; } // 返回某会话的消息数组(来自本地缓存),可能为空 async function readIdb(sessionId) { try { const db = await openDB(); return await new Promise((resolve) => { let out = null; try { const tx = db.transaction(CFG.STORE, 'readonly'); const store = tx.objectStore(CFG.STORE); const req = store.getAll(); req.onsuccess = () => { const recs = req.result || []; // 匹配当前会话:优先 record.key,其次 record.data.chat_session.id let rec = recs.find((r) => r && r.key === sessionId) || recs.find((r) => r && r.data && r.data.chat_session && r.data.chat_session.id === sessionId); if (rec) { const cm = (rec.data && rec.data.chat_messages) || rec.chat_messages; if (Array.isArray(cm)) out = cm; } resolve(out); }; req.onerror = () => resolve(null); } catch (e) { resolve(null); } }); } catch (e) { log('IDB 打开失败,走网络兜底', e.message); return null; } } // ---------- 写入 MSG_MAP ---------- function addMessage(m) { if (!m) return false; const id = (m.message_id != null) ? m.message_id : m.id; if (id == null) return false; const t = toMs(m.inserted_at != null ? m.inserted_at : m.insertedAt); if (!t) return false; MSG_MAP.set(Number(id), { date: new Date(t), role: (m.role || m.author_role || m.sender || ''), }); return true; } function ingestArray(arr) { if (!Array.isArray(arr)) return 0; let got = 0; for (const m of arr) if (addMessage(m)) got++; return got; } function ingestNet(json) { try { const biz = json && json.data && json.data.biz_data; const arr = biz && biz.chat_messages; return ingestArray(Array.isArray(arr) ? arr : null); } catch (e) { return 0; } } // ---------- 网络兜底:独立完整 history_messages(去掉 cache 参数) ---------- async function loadFullNet(sessionId, withCache0) { if (!sessionId || !CFG.netFallback) return 0; try { let url = `/api/v0/chat/history_messages?chat_session_id=${encodeURIComponent(sessionId)}`; if (withCache0) url += '&cache_version=0&cache_reset_at=0'; const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { accept: 'application/json' } }); const j = await r.json(); const got = ingestNet(j); if (got === 0 && !withCache0) return await loadFullNet(sessionId, true); return got; } catch (e) { log('网络兜底失败', e.message); return 0; } } // ---------- 加载当前会话 ---------- async function loadSession(sessionId) { if (!sessionId || busy) return; busy = true; try { const msgs = await readIdb(sessionId); let got = ingestArray(msgs || []); if (got > 0) { loaded = true; info(`已从浏览器本地库(IndexedDB)载入真实时间 ${got} 条`); } else if (CFG.netFallback) { const n = await loadFullNet(sessionId, false); if (n > 0) { loaded = true; info(`本地库为空,已通过网络兜底载入 ${n} 条`); } else info('该会话在本浏览器暂无缓存,也没拿到网络数据(可能从未在此浏览器打开过)'); } } catch (e) { log('loadSession 出错', e.message); } finally { busy = false; schedule(); } } // ---------- 收割应用自身的响应(新鲜首次加载时可能含完整 chat_messages) ---------- function harvest(text) { try { const j = JSON.parse(text); const biz = j && j.data && j.data.biz_data; const arr = biz && biz.chat_messages; if (Array.isArray(arr) && arr.length) { const got = ingestNet(j); if (got) { loaded = true; info(`从接口响应补充载入 ${got} 条`); schedule(); } } } catch (e) { /* 无关响应,忽略 */ } } const origFetch = window.fetch; window.fetch = function (...args) { const url = args[0] && (typeof args[0] === 'string' ? args[0] : args[0].url) || ''; return origFetch.apply(this, args).then((resp) => { if (typeof url === 'string' && url.includes('/api/v0/chat/history_messages')) { resp.clone().text().then(harvest).catch(() => {}); } return resp; }); }; const origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (m, u) { this.__url = u; return origOpen.apply(this, arguments); }; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function () { this.addEventListener('load', () => { const url = this.__url || ''; if (typeof url === 'string' && url.includes('/api/v0/chat/history_messages')) harvest(this.responseText); }); return origSend.apply(this, arguments); }; // ---------- 渲染 ---------- function isMsgRow(row) { return !!row.querySelector('[class*="message" i], [class*="markdown" i], [class*="bubble" i], [class*="collapsible" i]'); } function isBotRole(role) { const r = (role || '').toUpperCase(); return !!r && r !== 'USER'; } // 找 AI 回复底部那排操作图标容器(复制/编辑/重新生成/赞/踩/朗读/分享) // 特征:行的直接子元素中是 .ds-flex、且内含 >=2 个 role=button 的图标 function findActionBar(row) { const direct = [...row.children].filter((c) => c.classList && c.classList.contains('ds-flex')); const cand = direct.filter((c) => c.querySelectorAll('[role="button"]').length >= 2); if (cand.length) return cand[cand.length - 1]; const withBtns = [...row.children].filter((c) => c.querySelector('[role="button"]')); return withBtns.length ? withBtns[withBtns.length - 1] : null; } function tagOf(role) { const r = (role || '').toUpperCase(); if (r === 'USER') return ' · 你'; if (r === 'BOT') return ' · DeepSeek'; return ''; } function renderRow(row) { if (!row || !isMsgRow(row)) return; const key = row.getAttribute('data-virtual-list-item-key'); if (key == null) return; const id = Number(key); let el = row.querySelector('.ds-ts'); const info0 = MSG_MAP.get(id); if (info0) { const isBot = isBotRole(info0.role); if (!el) { el = document.createElement('div'); el.className = CFG.stampClass; } el.textContent = fmt(info0.date) + tagOf(info0.role); el.title = info0.date.toLocaleString(); el.classList.remove('pending'); if (isBot) { el.classList.add('bot'); const bar = findActionBar(row); if (bar) { // 塞进图标排里、弹性占位(flex:1)之前,和图标同一行 const spacer = [...bar.children].find((c) => { const s = (c.getAttribute('style') || ''); return s.includes('flex: 1') || s.includes('flex:1'); }); if (spacer) bar.insertBefore(el, spacer); else bar.appendChild(el); } else { row.appendChild(el); // 兜底:图标排还没渲染时先放行尾 } } else { el.classList.remove('bot'); if (el.parentElement !== row || el !== row.firstElementChild) row.insertBefore(el, row.firstChild); } return; } // 还没有真实时间:放淡占位(默认先放顶部,拿到真实时间后按角色自动挪位) if (!el) { el = document.createElement('div'); el.className = CFG.stampClass + ' pending'; el.textContent = '读取时间…'; row.insertBefore(el, row.firstChild); } if (curSession && !busy) scheduleRefresh(); } function scan() { document.querySelectorAll(CFG.rowSelector).forEach(renderRow); } let pending = false; function schedule() { if (pending) return; pending = true; requestAnimationFrame(() => { pending = false; scan(); }); } function scheduleRefresh() { if (refreshTimer) clearTimeout(refreshTimer); refreshTimer = setTimeout(() => { if (curSession && !busy) loadSession(curSession); }, 1500); } // ---------- 会话切换 ---------- function checkSession() { const s = sessionOf(); if (s === curSession) return; curSession = s; MSG_MAP.clear(); loaded = false; document.querySelectorAll('.' + CFG.stampClass).forEach((el) => el.remove()); if (curSession) { log('切换会话,已清空缓存并从本地库重新载入'); loadSession(curSession); } schedule(); } const _push = history.pushState, _rep = history.replaceState; history.pushState = function () { _push.apply(this, arguments); checkSession(); }; history.replaceState = function () { _rep.apply(this, arguments); checkSession(); }; window.addEventListener('popstate', checkSession); setInterval(checkSession, 2000); // ---------- 启动 ---------- function start() { if (!document.body) { requestAnimationFrame(start); return; } (document.head || document.documentElement).appendChild(style); checkSession(); new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true }); info('脚本已加载。每个对话的真实发送时间来自浏览器本地 IndexedDB(deepseek-chat / history-message)。打开/切换对话即自动标注。'); } start(); })();