// ==UserScript== // @name DS Enhance // @namespace https://github.com/calendar0917/DeepseekWeb-enhance // @version 5.0.0 // @description DeepSeek Chat 增强 — 文件夹管理、全文搜索、Prompt 管理器、会话笔记、导出、批量操作 // @author ds-enhance // @homepageURL https://github.com/calendar0917/DeepseekWeb-enhance // @supportURL https://github.com/calendar0917/DeepseekWeb-enhance/issues // @icon https://fe-static.deepseek.com/chat/favicon.svg // @match https://chat.deepseek.com/* // @grant none // @run-at document-start // @license GPL-3.0 // @noframes // ==/UserScript== "use strict"; (() => { // src/ds-enhance/core/injection.ts var CUSTOM_PROMPT_MARKER = "[自定义提示词]"; var lastInjectedSignature = null; var originalPushState = history.pushState; history.pushState = function(...args) { const newUrl = args[2]; if (newUrl) { const oldPath = location.pathname; const newPath = newUrl.toString().startsWith("http") ? new URL(newUrl).pathname : new URL(newUrl, location.origin).pathname; if (oldPath === "/" && newPath.startsWith("/s/")) { } else if (oldPath !== newPath) { lastInjectedSignature = null; } } return originalPushState.apply(this, args); }; window.addEventListener("popstate", () => { lastInjectedSignature = null; }); var cachedPrompts = []; function setCachedPrompts(prompts) { cachedPrompts = prompts; } function modifyRequest(bodyStr) { const enabled = cachedPrompts; const currentSignature = enabled.join("\n\n"); if (!currentSignature) { lastInjectedSignature = null; return bodyStr; } if (!bodyStr) return bodyStr; if (bodyStr.includes(CUSTOM_PROMPT_MARKER)) return bodyStr; if (lastInjectedSignature === currentSignature) { return bodyStr; } try { const parsed = JSON.parse(bodyStr); const tagged = `${CUSTOM_PROMPT_MARKER} ${currentSignature}`; let injected = false; if (parsed.prompt && typeof parsed.prompt === "string") { parsed.prompt = parsed.prompt + "\n\n" + tagged; injected = true; } if (parsed.messages?.length) { const lastIdx = parsed.messages.length - 1; parsed.messages[lastIdx].content = parsed.messages[lastIdx].content + "\n\n" + tagged; injected = true; } if (injected) { lastInjectedSignature = currentSignature; return JSON.stringify(parsed); } } catch { } return bodyStr; } function installXHRHook() { const XHRProto = XMLHttpRequest.prototype; const _origOpen = XHRProto.open; const _origSend = XHRProto.send; const _xhrMeta = /* @__PURE__ */ new WeakMap(); XHRProto.open = function(method, url, ...rest) { _xhrMeta.set(this, { url: typeof url === "string" ? url : url.href }); return _origOpen.apply(this, [method, url, ...rest]); }; XHRProto.send = function(body) { const meta = _xhrMeta.get(this); if (meta && meta.url.includes("completion") && body) { body = modifyRequest(body); } return _origSend.apply(this, [body]); }; } function installFetchHook() { const _origFetch = window.fetch; window.fetch = async function(...args) { const reqUrl = typeof args[0] === "string" ? args[0] : args[0] instanceof URL ? args[0].href : args[0]?.url; if (reqUrl && reqUrl.includes("completion") && args[1]?.body) { const body = args[1].body; if (typeof body === "string") { args[1].body = modifyRequest(body); } } return _origFetch.apply(this, args); }; } // src/shared/ui/dom.ts function injectCSS(css) { const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); } function waitForDOM() { return new Promise((resolve) => { if (document.body) resolve(); else new MutationObserver(() => { if (document.body) resolve(); }).observe(document.documentElement, { childList: true }); }); } // src/ds-enhance/db/storage.ts var DB_NAME = "ds-enhance"; var DB_VERSION = 1; function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db2 = req.result; if (!db2.objectStoreNames.contains("folders")) { const folderStore = db2.createObjectStore("folders", { keyPath: "id" }); folderStore.createIndex("parentId", "parentId", { unique: false }); } if (!db2.objectStoreNames.contains("sessionFolders")) { const sfStore = db2.createObjectStore("sessionFolders", { keyPath: "sessionId" }); sfStore.createIndex("folderId", "folderId", { unique: false }); } if (!db2.objectStoreNames.contains("bookmarks")) { db2.createObjectStore("bookmarks", { keyPath: "sessionId" }); } if (!db2.objectStoreNames.contains("notes")) { db2.createObjectStore("notes", { keyPath: "sessionId" }); } if (!db2.objectStoreNames.contains("messages")) { const msgStore = db2.createObjectStore("messages", { keyPath: "id" }); msgStore.createIndex("sessionId", "sessionId", { unique: false }); } if (!db2.objectStoreNames.contains("prompts")) { const pStore = db2.createObjectStore("prompts", { keyPath: "id" }); pStore.createIndex("mode", "mode", { unique: false }); } if (!db2.objectStoreNames.contains("promptChains")) { db2.createObjectStore("promptChains", { keyPath: "id" }); } if (!db2.objectStoreNames.contains("meta")) { db2.createObjectStore("meta", { keyPath: "key" }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } function dbGet(store, key) { return new Promise((resolve, reject) => { const req = store.get(key); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } function dbPut(store, value) { return new Promise((resolve, reject) => { const req = store.put(value); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } // src/ds-enhance/db/migrate.ts var MIGRATION_KEY = "localStorageMigrated"; async function migrateFromLocalStorage(db2) { const metaStore = db2.transaction("meta", "readonly").objectStore("meta"); const migrated = await dbGet(metaStore, MIGRATION_KEY); if (migrated) return; const tx = db2.transaction(["folders", "sessionFolders", "meta", "prompts"], "readwrite"); const folderStore = tx.objectStore("folders"); const sfStore = tx.objectStore("sessionFolders"); const metaStore2 = tx.objectStore("meta"); const promptStore = tx.objectStore("prompts"); try { const raw = localStorage.getItem("dse_categories"); if (raw) { const data = JSON.parse(raw); for (const cat of data.categories) { await dbPut(folderStore, { id: cat.id, name: cat.name, color: cat.color, parentId: null, createdAt: Date.now() }); } for (const [sessionId, catIds] of Object.entries(data.sessionMap)) { for (const catId of catIds) { await dbPut(sfStore, { sessionId, folderId: catId }); } } } } catch { } try { const raw = localStorage.getItem("dse_prompts"); if (raw) { const prompts = JSON.parse(raw); if (Array.isArray(prompts)) { for (const p of prompts) { await dbPut(promptStore, { id: p.id || `prompt_${Date.now()}_${Math.random().toString(36).slice(2)}`, name: p.name || "Untitled", content: p.content || "", mode: "inject", // legacy prompts were all "inject" mode enabled: p.enabled ?? true, createdAt: Date.now() }); } } } } catch { } try { const single = localStorage.getItem("dse_custom_prompt"); if (single?.trim()) { await dbPut(promptStore, { id: `prompt_legacy_${Date.now()}`, name: "Legacy Prompt", content: single.trim(), mode: "inject", enabled: true, createdAt: Date.now() }); } } catch { } await dbPut(metaStore2, { key: MIGRATION_KEY, migratedAt: Date.now() }); return new Promise((resolve, reject) => { tx.oncomplete = () => { console.log("[DS Enhance] localStorage → IndexedDB migration complete"); resolve(); }; tx.onerror = () => reject(tx.error); }); } // src/shared/ui/toast.ts var TOAST_COLORS = { info: "#2a2a3e", success: "#0d3320", error: "#3d0f0f" }; var _toastHistory = /* @__PURE__ */ new Map(); var TOAST_DEDUPE_WINDOW = 2500; function toast(msg, type = "info") { if (!document.body) return; const now = Date.now(); const key = `${type}:${msg}`; const last = _toastHistory.get(key) || 0; if (now - last < TOAST_DEDUPE_WINDOW) return; _toastHistory.set(key, now); if (_toastHistory.size > 100) { for (const [k, v] of _toastHistory) { if (now - v > TOAST_DEDUPE_WINDOW * 4) _toastHistory.delete(k); } } const el = document.createElement("div"); el.style.cssText = `position:fixed;bottom:24px;right:24px;z-index:2000001;background:${TOAST_COLORS[type]};color:#eee;padding:12px 22px;border-radius:10px;font-size:14px;box-shadow:0 4px 20px rgba(0,0,0,.5);font-family:system-ui;transition:opacity .3s;`; el.textContent = msg; document.body.appendChild(el); setTimeout(() => { el.style.opacity = "0"; setTimeout(() => el.remove(), 300); }, 3500); } // src/shared/utils/helpers.ts function esc(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; } function getSessionId() { const m = location.pathname.match(/\/s\/([a-f0-9-]+)/); return m ? m[1] : null; } // src/ds-enhance/core/api.ts var API = "https://chat.deepseek.com/api/v0"; function getToken() { try { const raw = localStorage.getItem("userToken"); if (!raw) return null; const p = JSON.parse(raw); return typeof p === "object" ? p.value || p.token || p : p; } catch { return localStorage.getItem("userToken"); } } async function api(path, method = "GET", body) { const token = getToken(); if (!token) throw new Error("未找到 userToken,请先登录 DeepSeek"); const opts = { method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "X-App-Version": "2025.04.25" } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(`${API}${path}`, opts); const json = await res.json(); if (json.code !== 0) throw new Error(json.msg || `API error ${json.code}`); return json.data; } async function fetchSessionsPage(cursor) { let url = "/chat_session/fetch_page?count=50"; if (cursor) url += `<e_cursor.pinned=${cursor.pinned}<e_cursor.updated_at=${cursor.updated_at}`; return api(url); } async function fetchAllSessions() { const sessions = []; let cursor = null; for (let i = 0; i < 100; i++) { const data = await fetchSessionsPage(cursor ?? void 0); const biz = data?.biz_data; const list = biz?.chat_sessions || []; sessions.push(...list); if (!biz?.has_more || !list.length) break; const last = list[list.length - 1]; cursor = { pinned: last.pinned ? 1 : 0, updated_at: last.updated_at }; } return sessions; } var apiDelete = (id) => api("/chat_session/delete", "POST", { chat_session_id: id }); var apiDeleteAll = () => api("/chat_session/delete_all", "POST"); var apiHistory = (id) => api(`/chat/history_messages?chat_session_id=${id}`); // src/ds-enhance/db/folders.ts async function getAllFolders(db2) { return new Promise((resolve, reject) => { const tx = db2.transaction("folders", "readonly"); const req = tx.objectStore("folders").getAll(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function getFolder(db2, id) { return new Promise((resolve, reject) => { const tx = db2.transaction("folders", "readonly"); const req = tx.objectStore("folders").get(id); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function createFolder(db2, name, color, parentId = null) { const folder = { id: "folder_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8), name, color, parentId, createdAt: Date.now() }; return new Promise((resolve, reject) => { const tx = db2.transaction("folders", "readwrite"); tx.objectStore("folders").put(folder); tx.oncomplete = () => resolve(folder); tx.onerror = () => reject(tx.error); }); } async function updateFolder(db2, id, updates) { const folder = await getFolder(db2, id); if (!folder) throw new Error("Folder not found"); Object.assign(folder, updates); return new Promise((resolve, reject) => { const tx = db2.transaction("folders", "readwrite"); tx.objectStore("folders").put(folder); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function deleteFolder(db2, id) { return new Promise((resolve, reject) => { const tx = db2.transaction(["folders", "sessionFolders"], "readwrite"); tx.objectStore("folders").delete(id); const sfStore = tx.objectStore("sessionFolders"); const idx = sfStore.index("folderId"); const req = idx.openCursor(id); req.onsuccess = () => { const cursor = req.result; if (cursor) { cursor.delete(); cursor.continue(); } }; tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function buildFolderTree(folders) { const map = /* @__PURE__ */ new Map(); const roots = []; for (const f of folders) { map.set(f.id, { ...f, children: [] }); } for (const f of folders) { const node = map.get(f.id); if (f.parentId && map.has(f.parentId)) { map.get(f.parentId).children.push(node); } else { roots.push(node); } } return roots; } // src/ds-enhance/ui/folders.ts function renderFolderPanel(container, db2, onFilterChange) { let activeFolderId = null; async function refresh() { const folders = await getAllFolders(db2); const tree = buildFolderTree(folders); container.innerHTML = ""; const addRow = document.createElement("div"); addRow.style.cssText = "display:flex;gap:6px;margin-bottom:10px"; addRow.innerHTML = ` `; container.appendChild(addRow); const nameInput = addRow.querySelector('input[type="text"]'); const colorInput = addRow.querySelector('input[type="color"]'); const addBtn = addRow.querySelector("button"); addBtn.onclick = async () => { const name = nameInput.value.trim(); if (!name) { toast("请输入文件夹名称", "error"); return; } await createFolder(db2, name, colorInput.value); nameInput.value = ""; toast(`已创建「${name}」`, "success"); await refresh(); }; const filterBar = document.createElement("div"); filterBar.style.cssText = "display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;align-items:center"; const allBtn = document.createElement("button"); allBtn.className = "dse-filter-btn"; allBtn.textContent = "全部"; if (!activeFolderId) allBtn.classList.add("active"); allBtn.onclick = () => { activeFolderId = null; onFilterChange(null); refresh(); }; filterBar.appendChild(allBtn); for (const folder of tree) { const btn = document.createElement("button"); btn.className = "dse-filter-btn"; btn.textContent = folder.name; btn.style.borderColor = folder.color; if (activeFolderId === folder.id) { btn.classList.add("active"); btn.style.background = folder.color + "33"; } btn.onclick = () => { activeFolderId = activeFolderId === folder.id ? null : folder.id; onFilterChange(activeFolderId); refresh(); }; filterBar.appendChild(btn); } container.appendChild(filterBar); const treeEl = document.createElement("div"); treeEl.style.cssText = "display:flex;flex-direction:column;gap:4px"; renderFolderTree(treeEl, tree, 0); container.appendChild(treeEl); } function renderFolderTree(container2, nodes, depth) { for (const node of nodes) { const row = document.createElement("div"); row.style.cssText = `display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;margin-left:${depth * 20}px;cursor:pointer;transition:background .1s`; row.onmouseenter = () => row.style.background = "#1e1e2e"; row.onmouseleave = () => row.style.background = ""; const dot = document.createElement("span"); dot.style.cssText = `width:10px;height:10px;border-radius:50%;background:${node.color};flex-shrink:0`; row.appendChild(dot); const name = document.createElement("span"); name.style.cssText = "flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"; name.textContent = node.name; row.appendChild(name); const editBtn = document.createElement("button"); editBtn.className = "btn-sm"; editBtn.textContent = "编辑"; editBtn.style.cssText = "background:none;border:none;color:#7aa2f7;cursor:pointer;font-size:11px;padding:2px 6px;border-radius:4px;opacity:0;transition:opacity .15s"; editBtn.onclick = (e) => { e.stopPropagation(); showEditDialog(node); }; row.appendChild(editBtn); const delBtn = document.createElement("button"); delBtn.className = "btn-sm"; delBtn.textContent = "删除"; delBtn.style.cssText = "background:none;border:none;color:#f87171;cursor:pointer;font-size:11px;padding:2px 6px;border-radius:4px;opacity:0;transition:opacity .15s"; delBtn.onclick = async (e) => { e.stopPropagation(); if (!confirm(`删除文件夹「${node.name}」?`)) return; await deleteFolder(db2, node.id); toast("已删除", "success"); await refresh(); }; row.appendChild(delBtn); row.onmouseenter = () => { row.style.background = "#1e1e2e"; editBtn.style.opacity = "1"; delBtn.style.opacity = "1"; }; row.onmouseleave = () => { row.style.background = ""; editBtn.style.opacity = "0"; delBtn.style.opacity = "0"; }; container2.appendChild(row); if (node.children.length) { renderFolderTree(container2, node.children, depth + 1); } } } function showEditDialog(node) { const bg = document.createElement("div"); bg.className = "dse-modal-bg"; bg.innerHTML = `
编辑文件夹
`; bg.querySelector(".cancel").onclick = () => bg.remove(); bg.onclick = (e) => { if (e.target === bg) bg.remove(); }; bg.querySelector(".confirm").onclick = async () => { const name = bg.querySelector("#edit-folder-name").value.trim(); const color = bg.querySelector("#edit-folder-color").value; if (!name) { toast("名称不能为空", "error"); return; } await updateFolder(db2, node.id, { name, color }); bg.remove(); toast("已保存", "success"); await refresh(); }; document.body.appendChild(bg); } return { refresh }; } // src/ds-enhance/core/search.ts async function indexSessionMessages(db2, sessionId, sessionTitle) { try { const hist = await apiHistory(sessionId); const msgs = hist?.biz_data?.chat_messages || []; const tx = db2.transaction("messages", "readwrite"); const store = tx.objectStore("messages"); const idx = store.index("sessionId"); const existing = await new Promise((resolve, reject) => { const req = idx.getAll(sessionId); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); for (const msg of existing) { store.delete(msg.id); } let count = 0; for (const m of msgs) { if (!m.content?.trim()) continue; const role = m.role?.toLowerCase(); if (role !== "user" && role !== "assistant") continue; const entry = { id: m.message_id, sessionId, sessionTitle, role, content: m.content, timestamp: m.timestamp || Date.now() / 1e3 }; store.put(entry); count++; } return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(count); tx.onerror = () => reject(tx.error); }); } catch { return 0; } } async function indexAllSessions(db2, sessions, onProgress) { let total = 0; for (let i = 0; i < sessions.length; i++) { const s = sessions[i]; const count = await indexSessionMessages(db2, s.id, s.title || "(无标题)"); total += count; onProgress?.(i + 1, sessions.length); } return total; } async function searchMessages(db2, query, filters) { if (!query.trim()) return []; const q = query.toLowerCase(); const results = []; return new Promise((resolve, reject) => { const tx = db2.transaction("messages", "readonly"); const store = tx.objectStore("messages"); const req = store.openCursor(); req.onsuccess = () => { const cursor = req.result; if (!cursor) { resolve(results.sort((a, b) => b.matchIndex - a.matchIndex)); return; } const msg = cursor.value; if (filters?.sessionId && msg.sessionId !== filters.sessionId) { cursor.continue(); return; } if (filters?.role && msg.role !== filters.role) { cursor.continue(); return; } const lowerContent = msg.content.toLowerCase(); const matchIdx = lowerContent.indexOf(q); if (matchIdx !== -1) { const start = Math.max(0, matchIdx - 50); const end = Math.min(msg.content.length, matchIdx + query.length + 50); let snippet = msg.content.slice(start, end); if (start > 0) snippet = "..." + snippet; if (end < msg.content.length) snippet = snippet + "..."; results.push({ messageId: msg.id, sessionId: msg.sessionId, sessionTitle: msg.sessionTitle, role: msg.role, content: msg.content, snippet, matchIndex: matchIdx }); } cursor.continue(); }; req.onerror = () => reject(req.error); }); } async function getIndexStats(db2) { return new Promise((resolve, reject) => { const tx = db2.transaction("messages", "readonly"); const store = tx.objectStore("messages"); const countReq = store.count(); countReq.onsuccess = () => { const sessionIds = /* @__PURE__ */ new Set(); const cursorReq = store.openCursor(); cursorReq.onsuccess = () => { const cursor = cursorReq.result; if (cursor) { sessionIds.add(cursor.value.sessionId); cursor.continue(); } else { resolve({ messageCount: countReq.result, sessionCount: sessionIds.size }); } }; cursorReq.onerror = () => reject(cursorReq.error); }; countReq.onerror = () => reject(countReq.error); }); } // src/ds-enhance/ui/search.ts function renderSearchPanel(container, db2) { let allSessions = []; container.innerHTML = `
`; const inputEl = container.querySelector("#search-input"); const roleEl = container.querySelector("#search-role"); const countEl = container.querySelector("#search-count"); const resultsEl = container.querySelector("#search-results"); const statsEl = container.querySelector("#search-stats"); async function updateStats() { try { const stats = await getIndexStats(db2); statsEl.textContent = `已索引 ${stats.sessionCount} 个对话, ${stats.messageCount} 条消息`; } catch { } } updateStats(); container.querySelector("#search-index").addEventListener("click", async () => { try { toast("正在建立索引...", "info"); const sessions = await fetchAllSessions(); const total = await indexAllSessions(db2, sessions, (current, total2) => { statsEl.textContent = `索引中 ${current}/${total2}...`; }); toast(`索引完成: ${total} 条消息`, "success"); updateStats(); } catch (e) { toast(`索引失败: ${e.message}`, "error"); } }); async function doSearch() { const query = inputEl.value.trim(); if (!query) { countEl.textContent = ""; resultsEl.innerHTML = ""; return; } const role = roleEl.value; resultsEl.innerHTML = '
搜索中...
'; try { const results = await searchMessages(db2, query, { role: role || void 0 }); countEl.textContent = `找到 ${results.length} 条匹配`; if (!results.length) { resultsEl.innerHTML = '
无匹配结果
'; return; } resultsEl.innerHTML = ""; for (const result of results.slice(0, 100)) { const row = document.createElement("div"); row.style.cssText = "padding:8px 10px;border-radius:8px;cursor:pointer;margin-bottom:4px;transition:background .1s"; row.onmouseenter = () => row.style.background = "#1e1e2e"; row.onmouseleave = () => row.style.background = ""; const header = document.createElement("div"); header.style.cssText = "display:flex;align-items:center;gap:6px;margin-bottom:4px"; const roleBadge = document.createElement("span"); roleBadge.style.cssText = `font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;color:${result.role === "user" ? "#7aa2f7" : "#4ade80"};background:${result.role === "user" ? "#1a2a4a" : "#0d3320"}`; roleBadge.textContent = result.role === "user" ? "User" : "AI"; header.appendChild(roleBadge); const title = document.createElement("span"); title.style.cssText = "font-size:12px;color:#888;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"; title.textContent = result.sessionTitle; header.appendChild(title); row.appendChild(header); const snippet = document.createElement("div"); snippet.style.cssText = "font-size:13px;color:#aaa;line-height:1.4"; snippet.innerHTML = highlightMatch(result.snippet, query); row.appendChild(snippet); row.onclick = () => { const url = `/a/chat/s/${result.sessionId}`; window.location.href = url; }; resultsEl.appendChild(row); } if (results.length > 100) { const more = document.createElement("div"); more.style.cssText = "text-align:center;padding:8px;font-size:12px;color:#666"; more.textContent = `... 还有 ${results.length - 100} 条结果`; resultsEl.appendChild(more); } } catch (e) { toast(`搜索失败: ${e.message}`, "error"); resultsEl.innerHTML = ""; } } let searchTimeout; inputEl.addEventListener("input", () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(doSearch, 300); }); roleEl.addEventListener("change", doSearch); } function highlightMatch(text, query) { const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`(${escaped})`, "gi"); return esc(text).replace(regex, '$1'); } // src/ds-enhance/db/prompts.ts async function getAllPrompts(db2) { return new Promise((resolve, reject) => { const tx = db2.transaction("prompts", "readonly"); const req = tx.objectStore("prompts").getAll(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function getPrompt(db2, id) { return new Promise((resolve, reject) => { const tx = db2.transaction("prompts", "readonly"); const req = tx.objectStore("prompts").get(id); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function createPrompt(db2, data) { const prompt = { ...data, id: "prompt_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8), variables: extractVariables(data.content), createdAt: Date.now() }; return new Promise((resolve, reject) => { const tx = db2.transaction("prompts", "readwrite"); tx.objectStore("prompts").put(prompt); tx.oncomplete = () => resolve(prompt); tx.onerror = () => reject(tx.error); }); } async function updatePrompt(db2, id, updates) { const prompt = await getPrompt(db2, id); if (!prompt) throw new Error("Prompt not found"); if (updates.content !== void 0) { prompt.variables = extractVariables(updates.content); } Object.assign(prompt, updates); return new Promise((resolve, reject) => { const tx = db2.transaction("prompts", "readwrite"); tx.objectStore("prompts").put(prompt); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function deletePrompt(db2, id) { return new Promise((resolve, reject) => { const tx = db2.transaction("prompts", "readwrite"); tx.objectStore("prompts").delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function extractVariables(content) { const matches = content.match(/\{\{([^}]+)\}\}/g); if (!matches) return []; return [...new Set(matches.map((m) => m.slice(2, -2).trim()))]; } async function getAllChains(db2) { return new Promise((resolve, reject) => { const tx = db2.transaction("promptChains", "readonly"); const req = tx.objectStore("promptChains").getAll(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function createChain(db2, name, steps) { const chain = { id: "chain_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8), name, steps, createdAt: Date.now() }; return new Promise((resolve, reject) => { const tx = db2.transaction("promptChains", "readwrite"); tx.objectStore("promptChains").put(chain); tx.oncomplete = () => resolve(chain); tx.onerror = () => reject(tx.error); }); } async function deleteChain(db2, id) { return new Promise((resolve, reject) => { const tx = db2.transaction("promptChains", "readwrite"); tx.objectStore("promptChains").delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } // src/ds-enhance/ui/prompts.ts function renderPromptPanel(container, db2) { let currentTab = "library"; container.innerHTML = `
`; const libView = container.querySelector("#prompt-lib-view"); const chainsView = container.querySelector("#prompt-chains-view"); container.querySelector("#prompt-tab-lib").addEventListener("click", () => { currentTab = "library"; libView.style.display = ""; chainsView.style.display = "none"; container.querySelector("#prompt-tab-lib").classList.add("active"); container.querySelector("#prompt-tab-chains").classList.remove("active"); refreshLibrary(); }); container.querySelector("#prompt-tab-chains").addEventListener("click", () => { currentTab = "chains"; libView.style.display = "none"; chainsView.style.display = ""; container.querySelector("#prompt-tab-lib").classList.remove("active"); container.querySelector("#prompt-tab-chains").classList.add("active"); refreshChains(); }); async function refreshLibrary() { const prompts = await getAllPrompts(db2); libView.innerHTML = ""; const addRow = document.createElement("div"); addRow.style.cssText = "display:flex;gap:6px;margin-bottom:10px"; addRow.innerHTML = ` `; libView.appendChild(addRow); addRow.querySelector("#prompt-add-btn").addEventListener("click", async () => { const name = addRow.querySelector("#prompt-add-name").value.trim(); if (!name) { toast("请输入名称", "error"); return; } await createPrompt(db2, { name, content: "", mode: "inject", enabled: true }); addRow.querySelector("#prompt-add-name").value = ""; toast(`已创建「${name}」`, "success"); refreshLibrary(); refreshInjectCache(); }); for (const prompt of prompts) { const card = document.createElement("div"); card.style.cssText = "background:#1a1a28;border:1px solid #333;border-radius:10px;padding:10px 12px;margin-bottom:8px;transition:border-color .15s"; if (!prompt.enabled) card.style.opacity = "0.5"; const header = document.createElement("div"); header.style.cssText = "display:flex;align-items:center;gap:8px"; const toggle = document.createElement("label"); toggle.style.cssText = "position:relative;width:32px;height:18px;flex-shrink:0;cursor:pointer"; toggle.innerHTML = ``; const toggleInput = toggle.querySelector("input"); toggleInput.addEventListener("change", async () => { await updatePrompt(db2, prompt.id, { enabled: toggleInput.checked }); refreshLibrary(); refreshInjectCache(); }); header.appendChild(toggle); const name = document.createElement("span"); name.style.cssText = "flex:1;font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"; name.textContent = prompt.name; header.appendChild(name); const modeBadge = document.createElement("span"); modeBadge.style.cssText = `font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;color:${prompt.mode === "inject" ? "#4ade80" : "#7aa2f7"};background:${prompt.mode === "inject" ? "#0d3320" : "#1a2a4a"}`; modeBadge.textContent = prompt.mode === "inject" ? "注入" : "插入"; header.appendChild(modeBadge); const delBtn = document.createElement("button"); delBtn.style.cssText = "background:none;border:none;color:#f87171;cursor:pointer;font-size:12px;padding:2px 6px;border-radius:4px"; delBtn.textContent = "删除"; delBtn.addEventListener("click", async () => { if (!confirm(`删除「${prompt.name}」?`)) return; await deletePrompt(db2, prompt.id); toast("已删除", "success"); refreshLibrary(); refreshInjectCache(); }); header.appendChild(delBtn); card.appendChild(header); const contentArea = document.createElement("div"); contentArea.style.cssText = "margin-top:8px;display:none"; const textarea = document.createElement("textarea"); textarea.style.cssText = "width:100%;padding:8px;border-radius:8px;border:1px solid #444;background:#16161e;color:#eee;font-size:12px;resize:vertical;min-height:60px;box-sizing:border-box;outline:none"; textarea.value = prompt.content; textarea.placeholder = "输入提示词内容... 使用 {{变量名}} 定义变量"; contentArea.appendChild(textarea); if (prompt.variables?.length) { const varsInfo = document.createElement("div"); varsInfo.style.cssText = "margin-top:6px;font-size:11px;color:#888"; varsInfo.textContent = `变量: ${prompt.variables.map((v) => `{{${v}}}`).join(", ")}`; contentArea.appendChild(varsInfo); } const modeRow = document.createElement("div"); modeRow.style.cssText = "display:flex;gap:6px;margin-top:6px;align-items:center"; modeRow.innerHTML = ` 模式: `; const modeSelect = modeRow.querySelector("select"); modeSelect.addEventListener("change", async () => { await updatePrompt(db2, prompt.id, { mode: modeSelect.value }); refreshLibrary(); refreshInjectCache(); }); contentArea.appendChild(modeRow); const saveBtn = document.createElement("button"); saveBtn.style.cssText = "margin-top:6px;padding:6px 12px;border-radius:8px;border:1px solid #444;background:#222;color:#eee;font-size:12px;cursor:pointer"; saveBtn.textContent = "保存"; saveBtn.addEventListener("click", async () => { await updatePrompt(db2, prompt.id, { content: textarea.value }); toast("已保存", "success"); refreshLibrary(); refreshInjectCache(); }); contentArea.appendChild(saveBtn); card.appendChild(contentArea); header.style.cursor = "pointer"; header.addEventListener("click", (e) => { if (e.target === toggleInput || e.target === delBtn) return; contentArea.style.display = contentArea.style.display === "none" ? "block" : "none"; }); libView.appendChild(card); } if (!prompts.length) { libView.innerHTML += '
暂无提示词,点击上方添加
'; } } async function refreshChains() { const chains = await getAllChains(db2); const prompts = await getAllPrompts(db2); chainsView.innerHTML = ""; const addRow = document.createElement("div"); addRow.style.cssText = "display:flex;gap:6px;margin-bottom:10px"; addRow.innerHTML = ` `; chainsView.appendChild(addRow); addRow.querySelector("#chain-add-btn").addEventListener("click", async () => { const name = addRow.querySelector("#chain-add-name").value.trim(); if (!name) { toast("请输入名称", "error"); return; } await createChain(db2, name, []); addRow.querySelector("#chain-add-name").value = ""; toast(`已创建「${name}」`, "success"); refreshChains(); }); for (const chain of chains) { const card = document.createElement("div"); card.style.cssText = "background:#1a1a28;border:1px solid #333;border-radius:10px;padding:10px 12px;margin-bottom:8px"; const header = document.createElement("div"); header.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer"; const nameEl = document.createElement("span"); nameEl.style.cssText = "flex:1;font-size:13px;font-weight:600"; nameEl.textContent = chain.name; header.appendChild(nameEl); const stepsCount = document.createElement("span"); stepsCount.style.cssText = "font-size:11px;color:#888"; stepsCount.textContent = `${chain.steps.length} 步`; header.appendChild(stepsCount); const delBtn = document.createElement("button"); delBtn.style.cssText = "background:none;border:none;color:#f87171;cursor:pointer;font-size:12px;padding:2px 6px;border-radius:4px"; delBtn.textContent = "删除"; delBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (!confirm(`删除「${chain.name}」?`)) return; await deleteChain(db2, chain.id); toast("已删除", "success"); refreshChains(); }); header.appendChild(delBtn); card.appendChild(header); const stepsArea = document.createElement("div"); stepsArea.style.cssText = "margin-top:8px;display:none"; for (let i = 0; i < chain.steps.length; i++) { const prompt = prompts.find((p) => p.id === chain.steps[i]); const stepRow = document.createElement("div"); stepRow.style.cssText = "display:flex;align-items:center;gap:6px;margin-bottom:4px"; const num = document.createElement("span"); num.style.cssText = "font-size:11px;color:#7aa2f7;font-weight:600;min-width:20px"; num.textContent = `${i + 1}.`; stepRow.appendChild(num); const name = document.createElement("span"); name.style.cssText = "flex:1;font-size:12px;color:#aaa"; name.textContent = prompt?.name || "(已删除)"; stepRow.appendChild(name); const removeBtn = document.createElement("button"); removeBtn.style.cssText = "background:none;border:none;color:#f87171;cursor:pointer;font-size:11px"; removeBtn.textContent = "×"; removeBtn.addEventListener("click", async () => { const newSteps = chain.steps.filter((_, idx) => idx !== i); await deleteChain(db2, chain.id); if (newSteps.length) { await createChain(db2, chain.name, newSteps); } refreshChains(); }); stepRow.appendChild(removeBtn); stepsArea.appendChild(stepRow); } const addStepRow = document.createElement("div"); addStepRow.style.cssText = "display:flex;gap:6px;margin-top:6px"; const stepSelect = document.createElement("select"); stepSelect.className = "dse-sel"; stepSelect.style.cssText = "flex:1;font-size:11px"; stepSelect.innerHTML = ''; for (const p of prompts) { stepSelect.innerHTML += ``; } stepSelect.addEventListener("change", async () => { if (!stepSelect.value) return; const newSteps = [...chain.steps, stepSelect.value]; await deleteChain(db2, chain.id); await createChain(db2, chain.name, newSteps); stepSelect.value = ""; refreshChains(); }); addStepRow.appendChild(stepSelect); stepsArea.appendChild(addStepRow); card.appendChild(stepsArea); header.addEventListener("click", () => { stepsArea.style.display = stepsArea.style.display === "none" ? "block" : "none"; }); chainsView.appendChild(card); } if (!chains.length) { chainsView.innerHTML += '
暂无 Chain
'; } } async function refreshInjectCache() { const prompts = await getAllPrompts(db2); const enabled = prompts.filter((p) => p.mode === "inject" && p.enabled).map((p) => p.content).filter(Boolean); setCachedPrompts(enabled); } refreshLibrary(); } // src/ds-enhance/db/notes.ts async function getNote(db2, sessionId) { return new Promise((resolve, reject) => { const tx = db2.transaction("notes", "readonly"); const req = tx.objectStore("notes").get(sessionId); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function saveNote(db2, sessionId, content) { const note = { sessionId, content, updatedAt: Date.now() }; return new Promise((resolve, reject) => { const tx = db2.transaction("notes", "readwrite"); tx.objectStore("notes").put(note); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function deleteNote(db2, sessionId) { return new Promise((resolve, reject) => { const tx = db2.transaction("notes", "readwrite"); tx.objectStore("notes").delete(sessionId); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } // src/ds-enhance/ui/notes.ts function renderNotesPanel(container, db2) { container.innerHTML = `
当前对话笔记
`; const contentEl = container.querySelector("#notes-content"); async function refresh() { const sessionId = getSessionId(); if (!sessionId) { contentEl.innerHTML = '
请先打开一个对话
'; return; } const note = await getNote(db2, sessionId); contentEl.innerHTML = `
${note?.updatedAt ? `最后更新: ${new Date(note.updatedAt).toLocaleString()}` : ""}
`; contentEl.querySelector("#notes-save").addEventListener("click", async () => { const text = contentEl.querySelector("#notes-textarea").value; await saveNote(db2, sessionId, text); toast("笔记已保存", "success"); refresh(); }); contentEl.querySelector("#notes-delete").addEventListener("click", async () => { if (!confirm("删除此笔记?")) return; await deleteNote(db2, sessionId); toast("笔记已删除", "success"); refresh(); }); } refresh(); } // src/ds-enhance/styles/panel.css.ts var DSE_PANEL_CSS = ` #dse-panel{position:fixed;z-index:999998;width:460px;max-height:75vh;background:#16161e;color:#eee;border:1px solid #333;border-radius:14px;box-shadow:0 8px 40px rgba(0,0,0,.6);font-family:system-ui;font-size:14px;display:none;flex-direction:column;overflow:hidden} #dse-panel.open{display:flex} #dse-panel .hd{padding:14px 18px;border-bottom:1px solid #2a2a3a;display:flex;align-items:center;justify-content:space-between;flex-shrink:0} #dse-panel .hd h3{margin:0;font-size:15px;font-weight:600} #dse-panel .hd .cls{background:none;border:none;color:#888;font-size:20px;cursor:pointer;padding:0 4px} #dse-panel .hd .cls:hover{color:#fff} #dse-tabs{display:flex;border-bottom:1px solid #2a2a3a;overflow-x:auto;scrollbar-width:none;flex-shrink:0} #dse-tabs::-webkit-scrollbar{display:none} #dse-tabs button{flex:0 0 auto;padding:9px 14px;background:none;border:none;color:#888;font-size:12px;cursor:pointer;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;white-space:nowrap} #dse-tabs button.active{color:#7aa2f7;border-bottom-color:#7aa2f7} #dse-tabs button:hover{color:#ccc} .dse-bd{flex:1;overflow-y:auto;padding:12px 14px} .dse-section{display:none}.dse-section.active{display:block} .dse-actions{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap} .dse-actions button{padding:6px 12px;border-radius:8px;border:1px solid #444;background:#222;color:#eee;font-size:12px;cursor:pointer;transition:background .15s} .dse-actions button:hover{background:#333} .dse-actions button.pri{background:#2563eb;border-color:#2563eb;color:#fff} .dse-actions button.pri:hover{background:#3b82f6} .dse-actions button.dng{background:#7f1d1d;border-color:#991b1b} .dse-actions button.dng:hover{background:#991b1b} .dse-input{width:100%;padding:8px 12px;border-radius:8px;border:1px solid #444;background:#1a1a28;color:#eee;font-size:13px;box-sizing:border-box;outline:none} .dse-input:focus{border-color:#7aa2f7} .dse-input::placeholder{color:#555} .dse-sel{padding:7px 10px;border:1px solid #444;border-radius:8px;background:#1a1a28;color:#eee;font-size:13px;outline:none} .dse-sel option{background:#1a1a28} .dse-row{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:8px;transition:background .1s} .dse-row:hover{background:#1e1e2e} .dse-row input[type=checkbox]{width:15px;height:15px;accent-color:#ef4444;cursor:pointer;flex-shrink:0} .dse-row .ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px} .dse-row .dt{font-size:11px;color:#555;flex-shrink:0} .dse-row .btn-sm{background:none;border:none;color:#7aa2f7;cursor:pointer;font-size:11px;flex-shrink:0;padding:2px 6px;border-radius:4px;opacity:0;transition:opacity .15s} .dse-row:hover .btn-sm{opacity:1} .dse-row .btn-sm:hover{background:#1a2a4a} .dse-catdot{width:10px;height:10px;border-radius:50%;cursor:pointer;transition:transform .1s} .dse-catdot:hover{transform:scale(1.3)} .dse-filter-btn{padding:4px 10px;border-radius:12px;border:1px solid #444;background:#222;color:#aaa;font-size:11px;cursor:pointer} .dse-filter-btn.active{border-color:#7aa2f7;color:#7aa2f7;background:#1a2a4a} .dse-prog{font-size:13px;color:#aaa;padding:8px 0} .dse-prog .bar{height:4px;background:#333;border-radius:2px;margin-top:6px;overflow:hidden} .dse-prog .bar-i{height:100%;background:#2563eb;border-radius:2px;transition:width .2s} .dse-modal-bg{position:fixed;inset:0;z-index:1000002;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center} .dse-modal-box{background:#1a1a28;color:#eee;border-radius:14px;padding:0;min-width:380px;max-width:520px;box-shadow:0 8px 40px rgba(0,0,0,.6);font-family:system-ui;overflow:hidden} .dse-modal-box .mhd{padding:16px 20px;border-bottom:1px solid #2a2a3a;font-size:15px;font-weight:600} .dse-modal-box .mbd{padding:14px 20px;max-height:360px;overflow-y:auto} .dse-modal-box .mft{padding:12px 20px;border-top:1px solid #2a2a3a;display:flex;justify-content:flex-end;gap:8px} .dse-modal-box .mft button{padding:8px 20px;border-radius:8px;border:none;cursor:pointer;font-size:13px} .dse-modal-box .mft .cancel{background:#333;color:#eee}.dse-modal-box .mft .cancel:hover{background:#444} .dse-modal-box .mft .confirm{background:#2563eb;color:#fff;font-weight:600}.dse-modal-box .mft .confirm:hover{background:#3b82f6} .mcp-label{font-size:12px;color:#888;margin-bottom:4px;display:block} `; // src/ds-enhance/index.ts async function loadPromptsForInjection() { try { const db2 = await openDB(); const tx = db2.transaction("prompts", "readonly"); const store = tx.objectStore("prompts"); const req = store.getAll(); req.onsuccess = () => { const all = req.result; const enabled = all.filter((p) => p.mode === "inject" && p.enabled).map((p) => p.content).filter(Boolean); setCachedPrompts(enabled); }; } catch { try { const arr = JSON.parse(localStorage.getItem("dse_prompts") || "[]"); if (Array.isArray(arr)) { setCachedPrompts(arr.filter((p) => p.enabled).map((p) => p.content).filter(Boolean)); } } catch { } } } loadPromptsForInjection(); installXHRHook(); installFetchHook(); setInterval(loadPromptsForInjection, 3e3); var db; waitForDOM().then(async () => { injectCSS(DSE_PANEL_CSS); db = await openDB(); await migrateFromLocalStorage(db); const fab = document.createElement("button"); fab.id = "dse-fab"; fab.innerHTML = "⚙"; fab.title = "DeepSeek 增强 (可拖动)"; fab.style.cssText = `position:fixed;z-index:999999;width:48px;height:48px;border-radius:50%;background:#2563eb;color:#fff;border:none;font-size:22px;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 12px rgba(37,99,235,.4);user-select:none;-webkit-user-select:none;touch-action:none`; document.body.appendChild(fab); const panel = document.createElement("div"); panel.id = "dse-panel"; panel.innerHTML = `

DeepSeek 增强

`; document.body.appendChild(panel); panel.querySelector(".cls").addEventListener("click", () => panel.classList.remove("open")); let fabDragged = false, fabSX = 0, fabSY = 0, fabOX = 0, fabOY = 0; const DRAG_TH = 5; function posPanel() { const r = fab.getBoundingClientRect(); let l = r.left; if (l + 460 > window.innerWidth - 10) l = window.innerWidth - 470; if (l < 10) l = 10; panel.style.left = l + "px"; panel.style.bottom = window.innerHeight - r.top + 10 + "px"; panel.style.top = "auto"; } fab.addEventListener("pointerdown", (e) => { if (e.button) return; fabDragged = false; fabSX = e.clientX; fabSY = e.clientY; const r = fab.getBoundingClientRect(); fabOX = e.clientX - r.left; fabOY = e.clientY - r.top; const mv = (ev) => { if (!fabDragged && Math.abs(ev.clientX - fabSX) + Math.abs(ev.clientY - fabSY) < DRAG_TH) return; fabDragged = true; fab.style.left = Math.max(0, Math.min(innerWidth - 48, ev.clientX - fabOX)) + "px"; fab.style.top = Math.max(0, Math.min(innerHeight - 48, ev.clientY - fabOY)) + "px"; fab.style.bottom = "auto"; }; const up = () => { document.removeEventListener("pointermove", mv); document.removeEventListener("pointerup", up); if (!fabDragged) { panel.classList.toggle("open"); if (panel.classList.contains("open")) posPanel(); } else if (panel.classList.contains("open")) { posPanel(); } }; document.addEventListener("pointermove", mv); document.addEventListener("pointerup", up); e.preventDefault(); }); fab.style.left = "20px"; fab.style.top = innerHeight - 68 + "px"; panel.querySelectorAll("#dse-tabs button").forEach((btn) => { btn.addEventListener("click", () => { panel.querySelectorAll("#dse-tabs button").forEach((b) => b.classList.remove("active")); btn.classList.add("active"); const tab = btn.dataset.tab; panel.querySelectorAll(".dse-section").forEach((s) => s.classList.remove("active")); panel.querySelector(`#sec-${tab}`).classList.add("active"); }); }); document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.shiftKey && e.key === "D") { e.preventDefault(); panel.classList.toggle("open"); if (panel.classList.contains("open")) posPanel(); } }); const foldersContainer = panel.querySelector("#sec-folders"); let activeFolderFilter = null; const folderPanel = renderFolderPanel( foldersContainer, db, (folderId) => { activeFolderFilter = folderId; } ); await folderPanel.refresh(); renderBatchTab(panel.querySelector("#sec-batch")); renderSearchPanel(panel.querySelector("#sec-search"), db); renderPromptPanel(panel.querySelector("#sec-prompt"), db); renderNotesPanel(panel.querySelector("#sec-notes"), db); toast("DS Enhance v5.0 已加载", "success"); }); function renderBatchTab(container) { container.innerHTML = `
`; const listEl = container.querySelector("#batch-list"); const statusEl = container.querySelector("#batch-status"); let allSessions = []; const selIds = /* @__PURE__ */ new Set(); function showProg(t, p) { statusEl.style.display = "block"; statusEl.innerHTML = `
${esc(t)}
`; } function hideProg() { statusEl.style.display = "none"; } function renderList() { listEl.innerHTML = ""; if (!allSessions.length) { listEl.innerHTML = '
暂无对话
'; return; } for (const s of allSessions) { const row = document.createElement("div"); row.className = "dse-row"; const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = selIds.has(s.id); cb.onchange = () => { if (cb.checked) selIds.add(s.id); else selIds.delete(s.id); }; row.appendChild(cb); const ttl = document.createElement("span"); ttl.className = "ttl"; ttl.textContent = s.title || "(无标题)"; row.appendChild(ttl); const dt = document.createElement("span"); dt.className = "dt"; dt.textContent = fmtDate(s.updated_at); row.appendChild(dt); listEl.appendChild(row); } } container.querySelector("#batch-load").addEventListener("click", async () => { try { listEl.innerHTML = '
加载中...
'; allSessions = await fetchAllSessions(); selIds.clear(); renderList(); toast(`已加载 ${allSessions.length} 条对话`, "success"); } catch (e) { toast(`加载失败: ${e.message}`, "error"); listEl.innerHTML = ""; } }); container.querySelector("#batch-sel-all").addEventListener("click", () => { allSessions.forEach((s) => selIds.add(s.id)); renderList(); }); container.querySelector("#batch-desel").addEventListener("click", () => { selIds.clear(); renderList(); }); container.querySelector("#batch-del").addEventListener("click", async () => { if (!selIds.size) { toast("请先选择", "error"); return; } if (!confirm(`确定删除 ${selIds.size} 条对话?不可撤销。`)) return; const ids = [...selIds]; let ok = 0, fail = 0; for (let i = 0; i < ids.length; i++) { showProg(`删除中 ${i + 1}/${ids.length}`, (i + 1) / ids.length * 100); try { await apiDelete(ids[i]); ok++; } catch { fail++; } } hideProg(); toast(`完成: 成功 ${ok}, 失败 ${fail}`, ok ? "success" : "error"); allSessions = await fetchAllSessions(); selIds.clear(); renderList(); }); container.querySelector("#batch-del-all").addEventListener("click", async () => { if (!confirm("⚠️ 删除【所有】对话?不可撤销!")) return; if (!confirm("再次确认!")) return; try { showProg("清空中...", 50); await apiDeleteAll(); hideProg(); toast("已清空", "success"); allSessions = []; selIds.clear(); renderList(); } catch (e) { hideProg(); toast(`失败: ${e.message}`, "error"); } }); } function fmtDate(ts) { if (!ts) return ""; const d = new Date(ts * 1e3); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } })();