// ==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")}`;
}
})();