// ==UserScript== // @name JSON Fetcher(JSON请求抓取,适用于Claude和ChatGPT,可以批量下载Claude历史对话) // @namespace https://github.com/alicewish/ // @version 3.0 // @description 满足各种需求 // @match *://yiyan.baidu.com/* // @match *://*.chatgpt.com/* // @match *://*.claude.ai/* // @match *://*.poe.com/* // @match *://gemini.google.com/* // @license MIT // @grant none // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/533496/JSON%20Fetcher%EF%BC%88JSON%E8%AF%B7%E6%B1%82%E6%8A%93%E5%8F%96%EF%BC%8C%E9%80%82%E7%94%A8%E4%BA%8EClaude%E5%92%8CChatGPT%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BDClaude%E5%8E%86%E5%8F%B2%E5%AF%B9%E8%AF%9D%EF%BC%89.user.js // @updateURL https://update.greasyfork.icu/scripts/533496/JSON%20Fetcher%EF%BC%88JSON%E8%AF%B7%E6%B1%82%E6%8A%93%E5%8F%96%EF%BC%8C%E9%80%82%E7%94%A8%E4%BA%8EClaude%E5%92%8CChatGPT%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BDClaude%E5%8E%86%E5%8F%B2%E5%AF%B9%E8%AF%9D%EF%BC%89.meta.js // ==/UserScript== (function () { 'use strict'; /************************************************************************ * 0. 统一按钮配置:集中管理所有图标、名称、提示 * (去除在暗色主题下会变彩色Emoji的字符,用 \uFE0E 或者非emoji字符来修正) ************************************************************************/ const BUTTON_MAP = { // 标题栏 & 通用操作相关 SCROLL_TOP: {icon: '↥', label: 'ScrollTop', title: '滚动到顶部'}, SCROLL_BOTTOM: {icon: '↧', label: 'ScrollBottom', title: '滚动到底部'}, MINIMIZE: {icon: '▁', label: 'Minimize', title: '最小化面板'}, RESTORE: {icon: '▔', label: 'Restore', title: '还原面板'}, CLOSE: {icon: '×', label: 'Close', title: '关闭面板'}, // 日志面板 DOWNLOAD_LOG: {icon: '📥', label: 'DownloadLog', title: '下载日志文件到本地'}, CLEAR_LOGS: {icon: '🗑️', label: 'ClearLogs', title: '清空全部日志'}, AUTO_SCROLL: {icon: '⤵️', label: 'AutoScroll', title: '自动滚动到最新日志开关'}, WRAP_LINES: {icon: '↩️', label: 'WrapLines', title: '日志换行开关'}, // JSON抓取面板 THEME_TOGGLE: {icon: '🌗', label: 'ThemeToggle', title: '切换亮/暗主题'}, TOGGLE_CAT: {icon: '⚙', label: 'ToggleCategory', title: '按分类显示或不分类'}, COPY_JSON: {icon: '📋', label: 'CopyJSON', title: '复制此JSON到剪贴板'}, DOWNLOAD_JSON: {icon: '⬇️', label: 'DownloadJSON', title: '下载此JSON文件'}, PREVIEW_JSON: {icon: '👁️', label: 'PreviewJSON', title: '预览此JSON'}, REMOVE_ITEM: {icon: '✂️', label: 'RemoveItem', title: '删除此条抓取记录'}, DOWNLOAD_ALL: {icon: '⬇️', label: 'DownloadAll', title: '批量下载'}, CLEAR_CATEGORY: {icon: '🗑️', label: 'ClearCategory', title: '清空此分类'}, SORT_ASC: {icon: '🔼', label: 'SortAsc', title: '升序排序'}, SORT_DESC: {icon: '🔽', label: 'SortDesc', title: '降序排序'}, // 特殊数据面板 TO_CSV: {icon: '⬇️表格', label: 'ToCSV', title: '导出所有解析数据为CSV'}, FOLD_ALL: {icon: '⏵', label: 'FoldAll', title: '折叠所有分类'}, UNFOLD_ALL: {icon: '⏷', label: 'UnfoldAll', title: '展开所有分类'}, DL_SINGLE: {icon: '⬇️', label: 'DownloadSingle', title: '下载此对话'}, TRASH: {icon: '🗑️', label: 'TrashAll', title: '清空所有解析数据'}, // 行内确认 CONFIRM_CHECK: {icon: '✔️', label: 'ConfirmYes', title: '确定'}, CONFIRM_CANCEL: {icon: '×', label: 'ConfirmNo', title: '取消'} }; /************************************************************************ * 1. 全局配置 / 常量(CONFIG) ************************************************************************/ const CONFIG = { // 初始面板位置/大小 initialPanels: { logPanel: {left: '400px', top: '100px', width: 420, height: 320}, jsonPanel: {left: '100px', top: '100px', width: 440, height: 500}, specPanel: {left: '600px', top: '100px', width: 460, height: 360} }, // 面板拖拽/缩放/吸附/透明度等 (不受主题影响) panelLimit: { defaultPanelOpacity: 0.95, // 面板默认不透明度 snapThreshold: 15, // 吸附像素范围 enableBackdropBlur: false // 若关闭,则强制不透明背景(主题透明度失效) }, // 额外功能限制或特性选项 features: { enableInlineConfirm: true, // 是否启用行内确认(替代系统confirm) maxLogEntries: 1000, // 日志最多保留多少条,超过后丢弃最旧的 maxJSONSizeKB: 0, // 若 >0 则提示过大JSON, 0 不限制 autoCleanupOnLarge: false // 若为true, 超过maxJSONSizeKB的JSON直接丢弃 }, // 是否在 JSON 面板标题中显示 PoW 难度(仅示例用) showPoWDifficulty: true, // 星标关键字(如 "VIP"、"myFav" 等) userStarKeywords: [], // Claude 列表 URL 正则 claudeListUrlPatterns: [ /\/api\/organizations\/[^/]+\/chat_conversations\?limit=10000$/i ], // Claude 批量下载选项 claudeBatchButtons: [ {label: '全部', days: Infinity, enabled: true, icon: '⇩全部'}, {label: '一天', days: 1, enabled: true, icon: '⬇️一天'}, {label: '三天', days: 3, enabled: true, icon: '⬇️三天'}, {label: '一周', days: 7, enabled: true, icon: '⬇️一周'}, {label: '一月', days: 30, enabled: true, icon: '⬇️一月'} ], // LocalStorage 键 logStorageKey: 'JSONInterceptorLogs', settingsStorageKey: 'JSONInterceptorSettings', panelStatePrefix: 'FloatingPanelState_', // 面板外观特效 (不受主题影响) panelEffects: { borderRadius: '8px', defaultBoxShadow: '0 5px 16px rgba(0,0,0,0.3)', hoverBoxShadow: '0 5px 24px rgba(0,0,0,0.4)', titlebarBottomBorder: 'rgba(68,68,68,0.07)', minimizedHeight: '36px' }, // 字号相关 (不受主题影响) fontSizes: { title: '16px', // 面板标题字号 content: '13px', // 面板正文字号 categoryTitle: '16px', // 分类标题字号(加大) categoryItem: '13px', // 分类子项字号 log: '12px', // 日志面板 inlineConfirm: '14px' // 行内确认提示 }, // 图标按钮尺寸相关 (不受主题影响) iconSizes: { titlebarButton: '14px', // 标题栏按钮 panelButton: '12px', categoryTitleButton: '14px', categoryItemButton: '12px' }, // 与布局/间距相关的通用设置(不受主题影响) layout: { // 行内确认 inlineConfirmPadding: '8px 12px', inlineConfirmButtonPadding: '2px 6px', // 面板拖拽把手 dragHandleSize: '18px', dragHandleMargin: '0 4px', // 面板内容区 floatingPanelContentPadding: '4px', // 分类及列表 categoryMargin: '8px', categoryHeaderPadding: '4px 8px', itemPadding: '4px 8px', // 进度条 progressBarHeight: '28px' }, // 主题颜色配置 (light/dark) themes: { light: { // 面板 panelTitleTextColor: '#333', panelTitleBgGradient: 'linear-gradient(to right, #b0c4de, #d8e6f3)', panelHandleColor: '#999', panelContentBg: 'rgba(255,255,255,0.7)', panelBorderColor: '#ccc', panelLogFontColor: '#222', panelJsonItemHoverBg: '#f9f9f9', panelHoverShadowColor: '0 5px 24px rgba(0,0,0,0.4)', // JSON高亮 highlightStringColor: '#ce9178', highlightNumberColor: '#b5cea8', highlightBooleanColor: '#569cd6', highlightNullColor: '#569cd6', highlightKeyColor: '#9cdcfe', // 特殊数据颜色 specialTitleColor: '#1f6feb', specialUuidColor: '#c678dd', specialUpdateColor: '#999', specialTaskColor: '#2b9371', // 进度条 progressBarBg: '#4caf50', progressBarTextColor: '#333', // 分类面板 categoryHeaderBg: '#f2f6fa', categoryBorderColor: '#ddd', itemHoverBg: '#f9f9f9', searchInputBorder: '#ccc', // 各类文字 panelBtnTextColor: '#333', categoryTitleColor: '#444', searchLabelColor: '#333', itemDividerColor: '#eee', panelMinimizeBtnColor: '#333', // 最小化按钮(在light主题下) panelCloseBtnColor: '#c00', // 关闭按钮(在light主题下) foldIconColor: '#333', panelReopenBtnBg: '#f0f0f0', // 日志 logMultiColor: true, logLevelColors: { debug: '#666', info: '#222', warn: 'orange', error: 'red' }, // 行内确认(InlineConfirm) inlineConfirmBg: 'rgba(30,30,30,0.85)', inlineConfirmText: '#fff', inlineConfirmBorder: 'rgba(0,0,0,0.3)', inlineConfirmYesBg: '#4caf50', inlineConfirmYesText: '#fff', inlineConfirmNoBg: '#f44336', inlineConfirmNoText: '#fff', // 新增:拖拽把手内阴影、按钮悬停背景等 dragHandleInnerShadow: 'inset 0 1px 2px rgba(255,255,255,0.4)', inlineConfirmBtnBg: 'rgba(255,255,255,0.07)', inlineConfirmBtnHoverBg: 'rgba(255,255,255,0.12)', floatingReopenBtnBorder: '#999', jsonUrlColor: '#666', jsonSizeColor: '#999', progressWrapBg: '#f8f8f899', panelBtnHoverBg: 'rgba(0, 0, 0, 0.1)' }, dark: { // 面板 panelTitleTextColor: '#f8f8f8', panelTitleBgGradient: 'linear-gradient(to right, #3a3a3a, #444)', panelHandleColor: '#aaa', panelContentBg: 'rgba(25,25,25,0.88)', panelBorderColor: '#555', panelLogFontColor: '#ddd', panelJsonItemHoverBg: '#444', panelHoverShadowColor: '0 5px 24px rgba(0,0,0,0.9)', // JSON高亮 highlightStringColor: '#eecd99', highlightNumberColor: '#cae3b0', highlightBooleanColor: '#7fc8f8', highlightNullColor: '#7fc8f8', highlightKeyColor: '#8fd2ff', // 特殊数据颜色 specialTitleColor: '#62a8ea', specialUuidColor: '#c78dea', specialUpdateColor: '#aaa', specialTaskColor: '#6ccdaf', // 进度条 progressBarBg: '#4caf50', progressBarTextColor: '#fff', // 分类面板 categoryHeaderBg: '#333', categoryBorderColor: '#444', itemHoverBg: '#4a4a4a', searchInputBorder: '#666', // 各类文字 panelBtnTextColor: '#ddd', categoryTitleColor: '#f0f0f0', searchLabelColor: '#ddd', itemDividerColor: '#444', panelMinimizeBtnColor: '#fff', // 最小化按钮(在dark主题下) panelCloseBtnColor: '#ff5555', // 关闭按钮(在dark主题下) foldIconColor: '#ddd', panelReopenBtnBg: '#444', // 日志 logMultiColor: true, logLevelColors: { debug: '#aaaaaa', info: '#ddd', warn: 'yellow', error: 'tomato' }, // 行内确认(InlineConfirm) inlineConfirmBg: 'rgba(80,80,80,0.85)', inlineConfirmText: '#fff', inlineConfirmBorder: 'rgba(255,255,255,0.3)', inlineConfirmYesBg: '#4caf50', inlineConfirmYesText: '#fff', inlineConfirmNoBg: '#f44336', inlineConfirmNoText: '#fff', // 新增 dragHandleInnerShadow: 'inset 0 1px 2px rgba(255,255,255,0.2)', inlineConfirmBtnBg: 'rgba(255,255,255,0.07)', inlineConfirmBtnHoverBg: 'rgba(255,255,255,0.12)', floatingReopenBtnBorder: '#999', jsonUrlColor: '#aaa', jsonSizeColor: '#999', progressWrapBg: '#6667', panelBtnHoverBg: 'rgba(255,255,255,0.1)' } }, // 默认主题 defaultTheme: 'light', // 已存在相同 URL 时的更新策略: 'larger' or 'time' captureUpdatePolicy: "larger", // 并发下载队列 downloadQueueOptions: { maxConcurrent: 3, maxRetry: 3, retryDelay: 1000 } }; /************************************************************************ * 2. 行内确认(inlineConfirm),代替系统 confirm 弹窗 ************************************************************************/ function inlineConfirm(question, onYes, onNo, timeoutMs = 5000) { if (!CONFIG.features.enableInlineConfirm) { // 如果不启用行内确认,直接执行onYes if (onYes) onYes(); return; } // 创建行内确认容器 const container = document.createElement('div'); container.className = 'inline-confirm-container'; container.innerHTML = `
${question}
`; document.body.appendChild(container); const yesBtn = container.querySelector('.inline-confirm-yes'); if (yesBtn) { yesBtn.addEventListener('click', () => { UILogger.logMessage(`(inlineConfirm) 用户选择:确认 => ${question}`, 'info'); if (onYes) onYes(); cleanup(); }); } const noBtn = container.querySelector('.inline-confirm-no'); if (noBtn) { noBtn.addEventListener('click', () => { UILogger.logMessage(`(inlineConfirm) 用户选择:取消 => ${question}`, 'info'); if (onNo) onNo(); cleanup(); }); } const timer = setTimeout(() => { UILogger.logMessage(`(inlineConfirm) 超时自动消失 => ${question}`, 'debug'); cleanup(); }, timeoutMs); function cleanup() { clearTimeout(timer); container.remove(); } } /************************************************************************ * 3. 通用函数(下载、JSON高亮、错误日志、复制等) ************************************************************************/ function downloadFile(text, fileName, mime = 'application/json') { try { if (!text) { UILogger.logMessage(`downloadFile警告: 内容为空,无法下载 -> ${fileName}`, 'warn'); return; } if (!fileName) { UILogger.logMessage(`downloadFile警告: 文件名为空, 使用默认download.json`, 'warn'); fileName = 'download.json'; } const blob = new Blob([text], {type: mime}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (err) { logErrorWithStack(err, 'downloadFile'); } } function highlightJson(str) { try { // 转义 HTML str = str.replace(/&/g, '&') .replace(//g, '>'); return str.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^"\\])*"(\s*:\s*)?|\b(true|false|null)\b|\b-?\d+(\.\d+)?([eE][+\-]?\d+)?\b)/g, match => { let cls = 'number'; if (/^"/.test(match)) { cls = /:$/.test(match) ? 'key' : 'string'; } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return `${match}`; } ); } catch (err) { logErrorWithStack(err, 'highlightJson'); return str; } } function logErrorWithStack(err, context = '') { const msg = `[ERROR] ${context ? (context + ': ') : ''}${err.message}\nStack: ${err.stack}`; UILogger.logMessage(msg, 'error'); console.error(err); } function copyText(str) { try { navigator.clipboard.writeText(str); UILogger.logMessage(`已复制文本到剪贴板`, 'info'); } catch (e) { UILogger.logMessage(`复制到剪贴板失败: ${e.message}`, 'error'); } } /************************************************************************ * 4. ZIndex & GlobalPanels 管理 ************************************************************************/ const ZIndexManager = { currentZIndex: 999999, /** * 将某个元素提升到最前 * @param {HTMLElement} el 目标元素 */ bringToFront(el) { this.currentZIndex++; el.style.zIndex = String(this.currentZIndex); } }; const GlobalPanels = { panels: [], register(panel) { this.panels.push(panel); }, unregister(panel) { const idx = this.panels.indexOf(panel); if (idx >= 0) this.panels.splice(idx, 1); }, getAllPanels() { return this.panels; } }; /************************************************************************ * 5. BaseFloatingPanel (面板基类) * 新增onDragStart/onDragEnd/onDestroy/onReopen,优化事件回调与日志 ************************************************************************/ class BaseFloatingPanel { /** * @param {Object} options 初始化选项 * @param {string} options.id 面板ID(用于保存/加载位置尺寸) * @param {string} options.title 面板标题 * @param {string|number} options.defaultLeft 初始left * @param {string|number} options.defaultTop 初始top * @param {number} options.defaultWidth 初始宽度 * @param {number} options.defaultHeight 初始高度 * @param {boolean} options.showReopenBtn 是否显示"重新打开"按钮 * @param {string} options.reopenBtnText 重新打开按钮文字 * @param {string} options.reopenBtnTop 重新打开按钮的top定位 * @param {boolean} options.allowResize 是否允许拖拽缩放 * @param {boolean} options.destroyOnClose 关闭后是否直接销毁DOM * @param {boolean} options.doubleClickTitleToToggleMaximize 是否双击标题栏自动最大化切换 * * @param {Function} options.onClose 关闭回调 * @param {Function} options.onMinimize 最小化回调 * @param {Function} options.onRestore 还原回调 * @param {Function} options.onFocus 面板获得焦点(点击)回调 * @param {Function} options.onOpen 面板初次打开时的回调 * @param {Function} options.onDestroy 面板真正destroy时的回调 * @param {Function} options.onReopen 面板重新打开时的回调 * @param {Function} options.onDragStart 拖拽开始回调 * @param {Function} options.onDragEnd 拖拽结束回调 */ constructor(options = {}) { const { id = '', title = '浮动面板', defaultLeft = '50px', defaultTop = '50px', defaultWidth = 300, defaultHeight = 200, showReopenBtn = true, reopenBtnText = '打开面板', reopenBtnTop = '10px', allowResize = true, destroyOnClose = false, doubleClickTitleToToggleMaximize = false, onClose = () => { }, onMinimize = () => { }, onRestore = () => { }, onFocus = () => { }, onOpen = () => { }, onDestroy = () => { }, onReopen = () => { }, onDragStart = () => { }, onDragEnd = () => { } } = options; // 保存初始化参数 this.id = id; this.title = title; this.showReopenBtn = showReopenBtn; this.reopenBtnText = reopenBtnText; this.reopenBtnTop = reopenBtnTop; this.allowResize = allowResize; this.destroyOnClose = destroyOnClose; this.doubleClickTitleToToggleMaximize = doubleClickTitleToToggleMaximize; // 回调 this.onClose = onClose; this.onMinimize = onMinimize; this.onRestore = onRestore; this.onFocus = onFocus; this.onOpen = onOpen; this.onDestroy = onDestroy; this.onReopen = onReopen; this.onDragStart = onDragStart; this.onDragEnd = onDragEnd; // 面板的状态记录 this.panelState = { minimized: false, closed: false, isMaximized: false, // 可选:是否最大化 left: defaultLeft, top: defaultTop, width: defaultWidth + 'px', height: defaultHeight + 'px', restoredHeight: defaultHeight + 'px' }; try { this.initDOM(defaultHeight); GlobalPanels.register(this); this.loadState(defaultHeight); this.initDragEvents(); this.initResizeObserver(); this.updatePanelBackgroundByTheme(); this.initTitlebarDoubleClick(); UILogger.logMessage(`[BaseFloatingPanel] 面板已创建并初始化: ${title}`, 'info'); this.onOpen(); // 初次创建时执行onOpen } catch (err) { logErrorWithStack(err, 'BaseFloatingPanel constructor'); } } /** * 快速创建一个按钮,根据 BUTTON_MAP 的配置 * @param {string} btnKey 对应 BUTTON_MAP 的键 * @param {Function} onClick 点击回调 * @returns {HTMLButtonElement} */ static createPanelButton(btnKey, onClick = null) { const cfg = BUTTON_MAP[btnKey]; if (!cfg) { UILogger.logMessage(`[createPanelButton] 未找到按钮配置: ${btnKey}`, 'warn'); const fallbackBtn = document.createElement('button'); fallbackBtn.textContent = btnKey; if (onClick) fallbackBtn.addEventListener('click', onClick); return fallbackBtn; } const btn = document.createElement('button'); btn.className = 'floating-panel-btn'; btn.textContent = cfg.icon; btn.title = cfg.title; if (onClick) { btn.addEventListener('click', onClick); } return btn; } /** * 初始化DOM结构 * @param {number} defaultHeight 面板默认高度 */ initDOM(defaultHeight) { // 主容器 this.container = document.createElement('div'); this.container.classList.add('floating-panel-container', 'floating-panel'); if (this.id) this.container.id = this.id; // 初始位置与尺寸 this.container.style.left = this.panelState.left; this.container.style.top = this.panelState.top; this.container.style.width = this.panelState.width; this.container.style.height = this.panelState.height; this.container.style.opacity = String(CONFIG.panelLimit.defaultPanelOpacity); // 如果不启用毛玻璃,则强制全不透明 if (!CONFIG.panelLimit.enableBackdropBlur) { const theme = UIManager?.globalSettings?.currentTheme || CONFIG.defaultTheme; const themeVars = CONFIG.themes[theme] || CONFIG.themes.light; let forcedBg = themeVars.panelContentBg; forcedBg = forcedBg.replace(/(\d+,\s*\d+,\s*\d+),\s*([\d\.]+)/, '$1,1'); // 透明度改为1 this.container.style.background = forcedBg; this.container.style.backdropFilter = 'none'; } if (!this.allowResize) { this.container.style.resize = 'none'; } // 标题栏 this.titlebar = document.createElement('div'); this.titlebar.className = 'floating-panel-titlebar'; // 拖拽把手 this.dragHandle = document.createElement('div'); this.dragHandle.className = 'floating-panel-drag-handle'; // 标题文本 this.titleSpan = document.createElement('span'); this.titleSpan.className = 'floating-panel-title'; this.titleSpan.textContent = this.title; // 标题栏按钮(右侧:滚动、最小化、关闭) this.btnScrollTop = BaseFloatingPanel.createPanelButton('SCROLL_TOP', () => this.scrollToTop()); this.btnScrollBottom = BaseFloatingPanel.createPanelButton('SCROLL_BOTTOM', () => this.scrollToBottom()); this.btnMinimize = BaseFloatingPanel.createPanelButton('MINIMIZE', () => this.toggleMinimize()); this.btnMinimize.classList.add('minimize-btn'); this.btnClose = BaseFloatingPanel.createPanelButton('CLOSE', () => this.close()); this.btnClose.classList.add('close-btn'); // 组装标题栏 const fragTitle = document.createDocumentFragment(); fragTitle.appendChild(this.dragHandle); fragTitle.appendChild(this.titleSpan); fragTitle.appendChild(this.btnScrollTop); fragTitle.appendChild(this.btnScrollBottom); fragTitle.appendChild(this.btnMinimize); fragTitle.appendChild(this.btnClose); this.titlebar.appendChild(fragTitle); // 内容区 this.contentEl = document.createElement('div'); this.contentEl.className = 'floating-panel-content'; this.contentEl.style.padding = CONFIG.layout.floatingPanelContentPadding; // 将标题栏和内容区插入容器 this.container.appendChild(this.titlebar); this.container.appendChild(this.contentEl); document.body.appendChild(this.container); // 重新打开按钮(默认隐藏) this.reopenBtn = document.createElement('button'); this.reopenBtn.className = 'floating-reopen-btn'; this.reopenBtn.textContent = this.reopenBtnText; this.reopenBtn.style.top = this.reopenBtnTop; this.reopenBtn.style.display = 'none'; // 默认隐藏 document.body.appendChild(this.reopenBtn); this.reopenBtn.addEventListener('click', () => this.reopen()); } /** * 是否允许双击标题栏实现最大化/还原 */ initTitlebarDoubleClick() { if (!this.doubleClickTitleToToggleMaximize) return; this.titlebar.addEventListener('dblclick', () => { this.toggleMaximize(); }); } /** * 切换最大化 / 还原 * 示例用:若不需要,可自行移除 */ toggleMaximize() { const isMax = this.panelState.isMaximized; if (!isMax) { // 记录当前rect const rect = this.container.getBoundingClientRect(); this.panelState.oldLeft = rect.left + 'px'; this.panelState.oldTop = rect.top + 'px'; this.panelState.oldWidth = rect.width + 'px'; this.panelState.oldHeight = rect.height + 'px'; this.container.style.left = '0px'; this.container.style.top = '0px'; this.container.style.width = window.innerWidth + 'px'; this.container.style.height = window.innerHeight + 'px'; this.panelState.isMaximized = true; UILogger.logMessage(`[BaseFloatingPanel] 最大化: ${this.title}`, 'info'); } else { // 还原 this.container.style.left = this.panelState.oldLeft; this.container.style.top = this.panelState.oldTop; this.container.style.width = this.panelState.oldWidth; this.container.style.height = this.panelState.oldHeight; this.panelState.isMaximized = false; UILogger.logMessage(`[BaseFloatingPanel] 取消最大化: ${this.title}`, 'info'); } } /** * 根据当前主题更新面板背景等 */ updatePanelBackgroundByTheme() { try { const theme = UIManager?.globalSettings?.currentTheme || CONFIG.defaultTheme; const themeVars = CONFIG.themes[theme] || CONFIG.themes.light; if (CONFIG.panelLimit.enableBackdropBlur) { this.container.style.backdropFilter = 'blur(4px)'; } else { this.container.style.backdropFilter = 'none'; } let bg = themeVars.panelContentBg; if (!CONFIG.panelLimit.enableBackdropBlur) { // 强制不透明 bg = bg.replace(/(\d+,\s*\d+,\s*\d+),\s*([\d\.]+)/, '$1,1'); } this.container.style.background = bg; } catch (err) { logErrorWithStack(err, 'updatePanelBackgroundByTheme'); } } /** * 初始化拖拽事件 */ initDragEvents() { let offsetX = 0, offsetY = 0; let startLeft = 0, startTop = 0; let mouseDown = false; const onMove = (e) => { if (!mouseDown) return; const deltaX = e.clientX - offsetX; const deltaY = e.clientY - offsetY; this.container.style.left = (startLeft + deltaX) + 'px'; this.container.style.top = (startTop + deltaY) + 'px'; }; const onUp = () => { if (!mouseDown) return; mouseDown = false; this.snapToEdges(); // 吸附 document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); this.saveState(); UILogger.logMessage(`[BaseFloatingPanel] 拖拽结束 => left=${this.container.style.left}, top=${this.container.style.top}`, 'debug'); this.onDragEnd(); }; this.dragHandle.addEventListener('mousedown', e => { e.preventDefault(); e.stopPropagation(); ZIndexManager.bringToFront(this.container); this.onFocus(); // 用户点击了面板 offsetX = e.clientX; offsetY = e.clientY; const rect = this.container.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; mouseDown = true; this.onDragStart(); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); UILogger.logMessage(`[BaseFloatingPanel] 开始拖拽: ${this.title}`, 'debug'); }); // 点击面板时置顶 this.container.addEventListener('mousedown', () => { ZIndexManager.bringToFront(this.container); this.onFocus(); }); } /** * 若支持 ResizeObserver,则监听面板 resize */ initResizeObserver() { if (!this.allowResize) return; if (typeof ResizeObserver !== 'function') return; try { this.resizeObserver = new ResizeObserver(() => { if (!this.panelState.minimized && !this.panelState.isMaximized) { const rect = this.container.getBoundingClientRect(); this.panelState.restoredHeight = rect.height + 'px'; } this.saveState(); }); this.resizeObserver.observe(this.container); } catch (err) { logErrorWithStack(err, 'initResizeObserver'); } } /** * 边缘吸附逻辑 */ snapToEdges() { try { const rect = this.container.getBoundingClientRect(); let left = rect.left; let top = rect.top; const sw = window.innerWidth; const sh = window.innerHeight; const t = CONFIG.panelLimit.snapThreshold; // 与窗口四边吸附 if (left < t) left = 0; else if (sw - (left + rect.width) < t) left = sw - rect.width; if (top < t) top = 0; else if (sh - (top + rect.height) < t) top = sh - rect.height; // 与其他面板吸附 const panels = GlobalPanels.getAllPanels(); for (const p of panels) { if (p === this || p.panelState.closed) continue; const r2 = p.container.getBoundingClientRect(); const dxLeft = Math.abs(left - r2.right); const dxRight = Math.abs((left + rect.width) - r2.left); const dyTop = Math.abs(top - r2.bottom); const dyBottom = Math.abs((top + rect.height) - r2.top); const horizontallyOverlap = (top + rect.height >= r2.top && top <= r2.bottom); const verticallyOverlap = (left + rect.width >= r2.left && left <= r2.right); if (dxLeft < t && horizontallyOverlap) { left = r2.right; } if (dxRight < t && horizontallyOverlap) { left = r2.left - rect.width; } if (dyTop < t && verticallyOverlap) { top = r2.bottom; } if (dyBottom < t && verticallyOverlap) { top = r2.top - rect.height; } } this.container.style.left = left + 'px'; this.container.style.top = top + 'px'; } catch (err) { logErrorWithStack(err, 'snapToEdges'); } } /** * 从 localStorage 中加载面板状态 * @param {number} defaultHeight */ loadState(defaultHeight) { if (!this.id) return; try { const key = CONFIG.panelStatePrefix + this.id; const saved = localStorage.getItem(key); if (!saved) return; const st = JSON.parse(saved); if (!st) return; Object.assign(this.panelState, st); // 兼容无效数据 if (!this.panelState.restoredHeight || parseInt(this.panelState.restoredHeight) < 10) { this.panelState.restoredHeight = defaultHeight + 'px'; } const { minimized, closed, left, top, width, height, restoredHeight, isMaximized } = this.panelState; this.container.style.left = left; this.container.style.top = top; this.container.style.width = width; this.container.style.height = minimized ? CONFIG.panelEffects.minimizedHeight : (restoredHeight || height); // 若最大化 if (isMaximized) { this.toggleMaximize(); // 恢复最大化 } // 若最小化 if (minimized) { this.container.classList.add('minimized'); this.contentEl.style.display = 'none'; this.btnMinimize.textContent = BUTTON_MAP.RESTORE.icon; this.btnMinimize.title = BUTTON_MAP.RESTORE.title; } // 若关闭 if (closed) { this.container.style.display = 'none'; if (this.showReopenBtn) { this.reopenBtn.style.display = 'block'; } } } catch (err) { logErrorWithStack(err, 'BaseFloatingPanel loadState'); } } /** * 将面板状态存储到 localStorage */ saveState() { if (!this.id) return; try { const rect = this.container.getBoundingClientRect(); this.panelState.left = this.container.style.left || (rect.left + 'px'); this.panelState.top = this.container.style.top || (rect.top + 'px'); this.panelState.width = this.container.style.width || (rect.width + 'px'); if (!this.panelState.minimized && !this.panelState.isMaximized) { this.panelState.restoredHeight = this.container.style.height || (rect.height + 'px'); } this.panelState.height = this.container.style.height || (rect.height + 'px'); localStorage.setItem(CONFIG.panelStatePrefix + this.id, JSON.stringify(this.panelState)); } catch (err) { logErrorWithStack(err, 'BaseFloatingPanel saveState'); } } /** * 设置面板标题 * @param {string} newTitle */ setTitle(newTitle) { this.titleSpan.textContent = newTitle; } /** * 切换面板最小化/还原 */ toggleMinimize() { const willMinimize = !this.panelState.minimized; if (willMinimize) { // 记录还原前的高度 const rect = this.container.getBoundingClientRect(); if (rect.height > 40) { this.panelState.restoredHeight = rect.height + 'px'; } this.panelState.minimized = true; this.container.classList.add('minimized'); this.container.style.height = CONFIG.panelEffects.minimizedHeight; this.contentEl.style.display = 'none'; this.btnMinimize.textContent = BUTTON_MAP.RESTORE.icon; this.btnMinimize.title = BUTTON_MAP.RESTORE.title; UILogger.logMessage(`[BaseFloatingPanel] 已最小化: ${this.title}`, 'info'); this.onMinimize(); } else { this.panelState.minimized = false; this.container.classList.remove('minimized'); const rh = this.panelState.restoredHeight || '200px'; this.container.style.height = rh; this.contentEl.style.display = 'block'; this.btnMinimize.textContent = BUTTON_MAP.MINIMIZE.icon; this.btnMinimize.title = BUTTON_MAP.MINIMIZE.title; UILogger.logMessage(`[BaseFloatingPanel] 已还原: ${this.title}`, 'info'); this.onRestore(); } this.saveState(); } /** * 关闭面板 */ close() { if (this.destroyOnClose) { // 直接销毁模式 UILogger.logMessage(`[BaseFloatingPanel] destroyOnClose => ${this.title}`, 'info'); this.onClose(); this.destroy(); return; } // 否则正常“关闭”逻辑 this.panelState.closed = true; this.panelState.minimized = false; this.container.style.display = 'none'; if (this.showReopenBtn) { this.reopenBtn.style.display = 'block'; } UILogger.logMessage(`[BaseFloatingPanel] 已关闭: ${this.title}`, 'info'); this.onClose(); this.saveState(); } /** * 重新打开面板 */ reopen() { this.panelState.closed = false; this.container.style.display = 'flex'; if (this.showReopenBtn) { this.reopenBtn.style.display = 'none'; } if (this.panelState.minimized) { this.container.classList.add('minimized'); this.container.style.height = CONFIG.panelEffects.minimizedHeight; this.contentEl.style.display = 'none'; this.btnMinimize.textContent = BUTTON_MAP.RESTORE.icon; this.btnMinimize.title = BUTTON_MAP.RESTORE.title; } else { this.container.classList.remove('minimized'); this.contentEl.style.display = 'block'; this.btnMinimize.textContent = BUTTON_MAP.MINIMIZE.icon; this.btnMinimize.title = BUTTON_MAP.MINIMIZE.title; this.container.style.height = this.panelState.restoredHeight; } this.updatePanelBackgroundByTheme(); this.saveState(); UILogger.logMessage(`[BaseFloatingPanel] 重新打开: ${this.title}`, 'info'); this.onReopen(); } /** * 完全销毁面板(从DOM中移除) */ destroy() { GlobalPanels.unregister(this); if (this.container) { this.container.remove(); } if (this.reopenBtn) { this.reopenBtn.remove(); } UILogger.logMessage(`[BaseFloatingPanel] 已销毁: ${this.title}`, 'info'); this.onDestroy(); } /** * 内容区滚动到顶部 */ scrollToTop() { this.contentEl.scrollTop = 0; UILogger.logMessage(`[BaseFloatingPanel] scrollToTop: ${this.title}`, 'debug'); } /** * 内容区滚动到底部 */ scrollToBottom() { this.contentEl.scrollTop = this.contentEl.scrollHeight; UILogger.logMessage(`[BaseFloatingPanel] scrollToBottom: ${this.title}`, 'debug'); } /** * 静态方法: 打开一个临时预览面板(可用于JSON或其他文本) * @param {string} title * @param {string} jsonString */ static openPreviewPanel(title, jsonString) { // 如果上一次还留有 window.__globalEphemeralPanel,就先销毁它 if (window.__globalEphemeralPanel) { window.__globalEphemeralPanel.destroy(); window.__globalEphemeralPanel = null; } const ephemeralPanel = new BaseFloatingPanel({ title: `JSON预览: ${title}`, defaultLeft: '120px', defaultTop: '120px', defaultWidth: 600, defaultHeight: 400, showReopenBtn: false, // 不需要“打开面板”按钮 destroyOnClose: true, // 关闭后直接销毁 onClose: () => { // 清空全局引用 if (window.__globalEphemeralPanel === ephemeralPanel) { window.__globalEphemeralPanel = null; } } }); let pretty = jsonString; try { const obj = JSON.parse(jsonString); pretty = JSON.stringify(obj, null, 2); } catch (e) { // 若 parse 失败,就保持原字符串 } const html = `
${highlightJson(pretty)}
`; ephemeralPanel.contentEl.innerHTML = `
${html}
`; ephemeralPanel.updatePanelBackgroundByTheme(); ephemeralPanel.container.style.zIndex = String(ZIndexManager.currentZIndex + 1); window.__globalEphemeralPanel = ephemeralPanel; UILogger.logMessage(`[BaseFloatingPanel] 打开临时预览面板 => ${title}`, 'info'); } } /************************************************************************ * 6. 并发下载队列(DownloadQueue) - 附加日志 ************************************************************************/ class DownloadQueue { /** * @param {Object} options * @param {number} options.maxConcurrent 并发数 * @param {number} options.maxRetry 重试次数 * @param {number} options.retryDelay 重试延时 */ constructor(options = {}) { this.maxConcurrent = options.maxConcurrent || 3; this.maxRetry = options.maxRetry || 3; this.retryDelay = options.retryDelay || 1000; this.queue = []; this.activeCount = 0; this.results = []; this.onProgress = (doneCount, total, task) => { }; this.onComplete = (successCount, failCount, results) => { }; } /** * 添加任务 * @param {any} taskInfo 任务信息(自定义) * @param {Function} taskFn 必须返回 Promise 的函数 */ addTask(taskInfo, taskFn) { this.queue.push({ info: taskInfo, fn: taskFn, retryCount: 0, success: false, error: null }); } start() { UILogger.logMessage(`[DownloadQueue] start: total=${this.queue.length}`, 'debug'); this.next(); } next() { if (this.queue.length === 0 && this.activeCount === 0) { const successCount = this.results.filter(r => r.success).length; const failCount = this.results.length - successCount; UILogger.logMessage(`[DownloadQueue] 完成: 成功=${successCount}, 失败=${failCount}`, failCount > 0 ? 'warn' : 'info'); this.onComplete(successCount, failCount, this.results); return; } if (this.activeCount >= this.maxConcurrent) return; const task = this.queue.shift(); if (!task) return; this.activeCount++; task.fn().then(() => { task.success = true; this.results.push(task); this.activeCount--; const doneCount = this.results.length; const totalCount = this.results.length + this.queue.length; this.onProgress(doneCount, totalCount, task); this.next(); }).catch(err => { task.retryCount++; task.error = err; if (task.retryCount <= this.maxRetry) { UILogger.logMessage(`[DownloadQueue] 任务失败, 重试(${task.retryCount}): ${err.message}`, 'warn'); setTimeout(() => { this.activeCount--; this.queue.unshift(task); this.next(); }, this.retryDelay); } else { UILogger.logMessage(`[DownloadQueue] 任务彻底失败: ${err.message}`, 'error'); this.results.push(task); this.activeCount--; const doneCount = this.results.length; const totalCount = this.results.length + this.queue.length; this.onProgress(doneCount, totalCount, task); this.next(); } }); this.next(); } } /************************************************************************ * 7. 日志系统(UILogger)、请求拦截器、PoW 解析 ************************************************************************/ const UILogger = { logEntries: [], logPanel: null, logListEl: null, autoScroll: true, wrapLines: false, init() { try { const saved = localStorage.getItem(CONFIG.logStorageKey); if (saved) { const arr = JSON.parse(saved); if (Array.isArray(arr)) { this.logEntries = arr; } } } catch (e) { } this.createLogPanel(); }, createLogPanel() { const initPos = CONFIG.initialPanels.logPanel; this.logPanel = new BaseFloatingPanel({ id: 'log-panel-container', title: '操作日志', defaultLeft: initPos.left, defaultTop: initPos.top, defaultWidth: initPos.width, defaultHeight: initPos.height, reopenBtnText: '打开日志面板', reopenBtnTop: '50px', allowResize: true, onClose: () => this.logMessage('日志面板已关闭', 'info'), onMinimize: () => this.logMessage('日志面板已最小化', 'info'), onRestore: () => this.logMessage('日志面板已还原', 'info'), onFocus: () => this.logMessage('日志面板获得焦点', 'debug'), onOpen: () => this.logMessage('日志面板创建完成', 'debug') }); // 顶部按钮:下载日志、清空日志、自动滚动、换行开关 const btnDownload = BaseFloatingPanel.createPanelButton('DOWNLOAD_LOG', () => this.downloadLogs()); const btnClear = BaseFloatingPanel.createPanelButton('CLEAR_LOGS', () => { inlineConfirm('确定要清空日志吗?此操作不可恢复。', () => { this.clearLogs(); this.logMessage('已清空日志', 'warn'); }); }); const btnAutoScroll = BaseFloatingPanel.createPanelButton('AUTO_SCROLL', () => { this.autoScroll = !this.autoScroll; this.logMessage(`自动滚动已切换为 ${this.autoScroll}`, 'info'); btnAutoScroll.style.opacity = this.autoScroll ? '1' : '0.5'; }); btnAutoScroll.style.opacity = this.autoScroll ? '1' : '0.5'; const btnWrap = BaseFloatingPanel.createPanelButton('WRAP_LINES', () => { this.wrapLines = !this.wrapLines; this.updateWrapMode(); this.logMessage(`换行模式已切换为 ${this.wrapLines}`, 'info'); btnWrap.style.opacity = this.wrapLines ? '1' : '0.5'; }); btnWrap.style.opacity = this.wrapLines ? '1' : '0.5'; const fragTitle = document.createDocumentFragment(); fragTitle.appendChild(btnDownload); fragTitle.appendChild(btnClear); fragTitle.appendChild(btnAutoScroll); fragTitle.appendChild(btnWrap); this.logPanel.titlebar.insertBefore(fragTitle, this.logPanel.btnMinimize); // 日志列表 const ul = document.createElement('ul'); ul.className = 'log-panel-list'; this.logListEl = ul; this.logPanel.contentEl.appendChild(ul); // 加载旧日志 this.logEntries.forEach(ent => { const level = this.getLogLevel(ent); ul.appendChild(this.createLogLi(ent, level)); }); this.scrollToBottomIfNeeded(); }, updateWrapMode() { if (!this.logListEl) return; if (this.wrapLines) { this.logListEl.classList.add('wrap-lines'); } else { this.logListEl.classList.remove('wrap-lines'); } }, /** * 记录日志 * @param {string} msg * @param {string} level debug/info/warn/error */ logMessage(msg, level = 'info') { const timeStr = new Date().toLocaleTimeString(); const line = `[${timeStr}][${level}] ${msg}`; this.logEntries.push(line); // 若超出最大限制,则移除最旧 if (CONFIG.features.maxLogEntries > 0 && this.logEntries.length > CONFIG.features.maxLogEntries) { this.logEntries.splice(0, this.logEntries.length - CONFIG.features.maxLogEntries); } try { localStorage.setItem(CONFIG.logStorageKey, JSON.stringify(this.logEntries)); } catch (e) { } if (this.logListEl) { const li = this.createLogLi(line, level); this.logListEl.appendChild(li); this.scrollToBottomIfNeeded(); } }, getLogLevel(line) { const re = /^\[.+?\]\[([^]+?)\]/; const m = line.match(re); if (m) return m[1]; return 'info'; }, createLogLi(line, level = 'info') { const li = document.createElement('li'); li.className = 'log-line'; const themeName = UIManager?.globalSettings?.currentTheme || CONFIG.defaultTheme; const themeVars = CONFIG.themes[themeName] || CONFIG.themes.light; const multiColor = themeVars.logMultiColor !== false; if (multiColor) { // 拆分出时间、级别、消息 const re = /^\[([^]+?)\]\[([^]+?)\]\s(.*)$/; const m = re.exec(line); if (m) { const [_, timePart, lvlPart, msgPart] = m; const timeSpan = document.createElement('span'); timeSpan.style.color = '#999'; timeSpan.textContent = `[${timePart}]`; const lvlSpan = document.createElement('span'); const lvlCol = themeVars.logLevelColors[level] || '#000'; lvlSpan.style.color = lvlCol; lvlSpan.textContent = `[${lvlPart}]`; const msgSpan = document.createElement('span'); msgSpan.style.marginLeft = '4px'; msgSpan.textContent = msgPart; li.appendChild(timeSpan); li.appendChild(lvlSpan); li.appendChild(msgSpan); } else { li.textContent = line; } } else { li.textContent = line; } return li; }, scrollToBottomIfNeeded() { if (!this.autoScroll || !this.logListEl) return; setTimeout(() => { this.logListEl.scrollTop = this.logListEl.scrollHeight; }, 0); }, downloadLogs() { const t = document.title.replace(/[\\/:*?"<>|]/g, '_') || 'log'; const now = new Date(); const y = now.getFullYear(); const M = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); const fn = `${t}-${y}${M}${d}-${hh}${mm}${ss}.log`; const txt = this.logEntries.join('\n'); downloadFile(txt, fn, 'text/plain'); this.logMessage(`日志已下载: ${fn}`, 'info'); }, clearLogs() { this.logEntries = []; try { localStorage.setItem(CONFIG.logStorageKey, '[]'); } catch (e) { } if (this.logListEl) { this.logListEl.innerHTML = ''; } } }; // 请求拦截器 const RequestInterceptor = { capturedRequests: [], starUuid: '', init() { this.overrideXHR(); this.overrideFetch(); }, overrideXHR() { const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._requestMethod = method; this._requestUrl = url; return origOpen.apply(this, [method, url, ...rest]); }; XMLHttpRequest.prototype.send = function (...args) { this.addEventListener('loadend', () => { try { const ct = this.getResponseHeader('content-type') || ''; if (RequestInterceptor.isJson(ct) && RequestInterceptor.shouldCapture(this._requestUrl)) { const respText = this.responseText; const status = this.status; const cLen = this.getResponseHeader('Content-Length'); let headersObj = {}; if (cLen) headersObj['Content-Length'] = cLen; RequestInterceptor.addCaptured( this._requestUrl, respText, this._requestMethod, status, headersObj ); } } catch (e) { UILogger.logMessage(`XHR抓取异常: ${e.message}`, 'error'); } }); return origSend.apply(this, args); }; }, overrideFetch() { if (!window.fetch) return; const origFetch = window.fetch; window.fetch = async (input, init) => { const fetchP = origFetch(input, init); try { const url = (typeof input === 'string') ? input : (input.url || ''); const resp = await fetchP; const ct = resp.headers.get('content-type') || ''; const status = resp.status; const cLen = resp.headers.get('content-length'); let headersObj = {}; if (cLen) headersObj['Content-Length'] = cLen; if (this.isJson(ct) && this.shouldCapture(url)) { const cloneResp = resp.clone(); const text = await cloneResp.text(); const method = (init && init.method) || 'GET'; this.addCaptured(url, text, method, status, headersObj); } return resp; } catch (e) { UILogger.logMessage(`fetch抓取异常: ${e.message}`, 'error'); return fetchP; } }; }, isJson(ct) { return ct.toLowerCase().includes('application/json'); }, shouldCapture(url) { return !!url; }, findCapturedItemByUrl(url) { return this.capturedRequests.find(it => it.url === url); }, addCaptured(url, content, method, status, headersObj) { const sizeKB = content.length / 1024; if (CONFIG.features.maxJSONSizeKB > 0 && sizeKB > CONFIG.features.maxJSONSizeKB) { if (CONFIG.features.autoCleanupOnLarge) { UILogger.logMessage(`过大JSON已跳过(自动丢弃): ${url}`, 'warn'); return; } else { UILogger.logMessage(`捕获到过大JSON(${sizeKB.toFixed(2)}KB): ${url}`, 'warn'); } } const existing = this.findCapturedItemByUrl(url); if (existing) { // 若已存在则按策略更新 const policy = CONFIG.captureUpdatePolicy; if (policy === 'larger') { if (content.length > existing.content.length) { existing.content = content; existing.sizeKB = sizeKB.toFixed(2); existing.method = method; existing.status = status; existing.headersObj = headersObj; UILogger.logMessage(`更新捕获(更大JSON): ${url}`, 'debug'); } else { UILogger.logMessage(`已捕获且更小或相等,跳过: ${url}`, 'debug'); } } else if (policy === 'time') { existing.content = content; existing.sizeKB = sizeKB.toFixed(2); existing.method = method; existing.status = status; existing.headersObj = headersObj; UILogger.logMessage(`更新捕获(时间更新): ${url}`, 'debug'); } return; } let fn = url.split('/').pop().split('?')[0] || 'download'; try { fn = decodeURIComponent(fn); } catch (e) { } const kb = sizeKB.toFixed(2); let category = 'other'; if (this.isStarUrl(url, fn)) { category = 'star'; } else if (/\/backend-api\//i.test(url)) { category = 'backend'; } else if (/^https?:\/\/[^/]*api\./i.test(url)) { category = 'api'; } else if (/^https?:\/\/[^/]*public\./i.test(url)) { category = 'public'; } const item = { url, content, filename: fn, sizeKB: kb, method, status, headersObj, category }; this.capturedRequests.push(item); UILogger.logMessage(`捕获JSON (${method}) [${status || '--'}]: ${url}`, 'info'); PoWParser.checkDifficulty(content); SpecialDataParser.parse(url, content); UIManager.updateLists(); }, isStarUrl(url, filename) { if (this.starUuid && url.toLowerCase().includes(this.starUuid.toLowerCase())) { return true; } for (const re of CONFIG.claudeListUrlPatterns) { if (re.test(url)) return true; } if (CONFIG.userStarKeywords && CONFIG.userStarKeywords.length > 0) { const lf = filename.toLowerCase(); for (const kw of CONFIG.userStarKeywords) { if (kw && lf.includes(kw.toLowerCase())) { return true; } } } return false; } }; // PoW 解析示例(可根据需要定制) const PoWParser = { currentDifficulty: '', checkDifficulty(raw) { if (!CONFIG.showPoWDifficulty) return; try { const parsed = JSON.parse(raw); if (parsed.proofofwork && parsed.proofofwork.difficulty) { this.currentDifficulty = parsed.proofofwork.difficulty; UIManager.refreshJsonPanelTitle(); } } catch (e) { } } }; /************************************************************************ * 8. SpecialDataParser(Claude/ChatGPT) - 特殊数据解析 ************************************************************************/ const SpecialDataParser = { claudeConvData: [], chatgptConvData: [], chatgptTasksData: [], parse(reqUrl, raw) { // 解析Claude列表 for (const re of CONFIG.claudeListUrlPatterns) { if (re.test(reqUrl)) { this.parseClaudeArray(reqUrl, raw); UIManager.updateSpecialDataPanel(); return; } } // 解析ChatGPT对话列表 if (/\/backend-api\/conversations\?/i.test(reqUrl)) { this.parseChatGPTList(raw); UIManager.updateSpecialDataPanel(); return; } // 解析ChatGPT任务 if (/\/backend-api\/tasks$/i.test(reqUrl)) { this.parseChatGPTTasks(raw); UIManager.updateSpecialDataPanel(); return; } }, parseClaudeArray(reqUrl, raw) { try { const parsed = JSON.parse(raw); const arr = Array.isArray(parsed) ? parsed : parsed.data; if (!Array.isArray(arr)) return; let orgUuid = ''; const m = /\/api\/organizations\/([^/]+)/i.exec(reqUrl); if (m) orgUuid = m[1]; arr.forEach(item => { const {uuid, name, updated_at} = item; const shTime = this.toShanghai(updated_at); let convUrl = ''; if (orgUuid && uuid) { convUrl = `/api/organizations/${orgUuid}/chat_conversations/${uuid}?tree=True&rendering_mode=messages&render_all_tools=true`; } this.claudeConvData.push({ uuid, name, updated_at_shanghai: shTime, convUrl }); }); UILogger.logMessage(`解析Claude列表: 共${arr.length}条`, 'info'); } catch (e) { UILogger.logMessage(`解析Claude异常: ${e.message}`, 'error'); } }, parseChatGPTList(raw) { try { const obj = JSON.parse(raw); if (!obj || !Array.isArray(obj.items)) return; obj.items.forEach(item => { const {id, title, update_time} = item; const shTime = this.toShanghai(update_time); let convUrl = ''; if (id) { convUrl = `https://chatgpt.com/backend-api/conversation/${id}`; } this.chatgptConvData.push({ id, title, update_time_shanghai: shTime, convUrl }); }); UILogger.logMessage(`解析ChatGPT对话: 共${obj.items.length}条`, 'info'); } catch (e) { UILogger.logMessage(`解析ChatGPT异常: ${e.message}`, 'error'); } }, parseChatGPTTasks(raw) { try { const obj = JSON.parse(raw); if (!obj || !Array.isArray(obj.tasks)) return; obj.tasks.forEach(task => { this.chatgptTasksData.push({ title: task.title || '', task_id: task.task_id || '', updated_at_shanghai: this.toShanghai(task.updated_at), conversation_id: task.conversation_id || '', original_conversation_id: task.original_conversation_id || '' }); }); UILogger.logMessage(`解析ChatGPT任务: 当前累计 ${this.chatgptTasksData.length} 条`, 'info'); } catch (e) { UILogger.logMessage(`解析ChatGPT任务异常: ${e.message}`, 'error'); } }, toShanghai(iso) { if (!iso) return ''; try { const d = new Date(iso); if (isNaN(d.getTime())) return iso; return d.toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai'}); } catch (e) { return iso; } }, async downloadClaudeConversation(item) { if (!item || !item.convUrl) { UILogger.logMessage(`Claude对话下载失败: convUrl为空 => ${JSON.stringify(item)}`, 'error'); throw new Error(`convUrl not found for item: ${item?.name || ''}`); } const {convUrl, name = '', uuid = ''} = item; UILogger.logMessage(`[Claude] 开始下载对话: name=${name}, uuid=${uuid}, url=${convUrl}`, 'debug'); let resp; try { resp = await fetch(convUrl); } catch (fetchErr) { UILogger.logMessage(`[Claude] 对话请求异常: ${fetchErr.message}`, 'error'); throw fetchErr; } if (!resp.ok) { UILogger.logMessage(`[Claude] 对话请求失败: HTTP ${resp.status} => ${convUrl}`, 'error'); throw new Error(`Claude对话下载失败: HTTP ${resp.status} - ${name}-${uuid}`); } const txt = await resp.text(); UILogger.logMessage(`[Claude] 对话下载成功: name=${name}, uuid=${uuid}, length=${txt.length}`, 'debug'); let safeName = name.replace(/[\\/:*?"<>|]/g, '_') || 'claude-conv'; if (uuid) safeName += '-' + uuid; if (!safeName.endsWith('.json')) safeName += '.json'; downloadFile(txt, safeName); }, async downloadChatGPTConversation(item) { if (!item || !item.convUrl) { UILogger.logMessage(`ChatGPT对话下载失败: convUrl为空 => ${JSON.stringify(item)}`, 'error'); throw new Error(`convUrl not found for ChatGPT item: ${item?.title || ''}`); } const {convUrl, title = '', id = ''} = item; UILogger.logMessage(`[ChatGPT] 开始下载对话: title=${title}, id=${id}, url=${convUrl}`, 'debug'); let resp; try { resp = await fetch(convUrl); } catch (err) { UILogger.logMessage(`[ChatGPT] 对话请求异常: ${err.message}`, 'error'); throw err; } if (!resp.ok) { UILogger.logMessage(`[ChatGPT] 对话请求失败: HTTP ${resp.status} => ${convUrl}`, 'error'); throw new Error(`ChatGPT对话下载失败: HTTP ${resp.status} - ${title}-${id}`); } const txt = await resp.text(); UILogger.logMessage(`[ChatGPT] 对话下载成功: title=${title}, id=${id}, length=${txt.length}`, 'debug'); let safeTitle = title.replace(/[\\/:*?"<>|]/g, '_') || 'chatgpt-conv'; let fileName = safeTitle; if (id) fileName += '-' + id; if (!fileName.endsWith('.json')) fileName += '.json'; downloadFile(txt, fileName); } }; /************************************************************************ * 9. UIManager: 生成 JSON面板 & 特殊数据面板 ************************************************************************/ const UIManager = { globalSettings: {useCategories: true, currentTheme: CONFIG.defaultTheme}, currentSearchText: '', init() { try { const saved = localStorage.getItem(CONFIG.settingsStorageKey); if (saved) { const obj = JSON.parse(saved); if (obj) this.globalSettings = obj; } } catch (e) { } this.applyTheme(this.globalSettings.currentTheme); this.applyDimensionsAndEffects(); this.createJsonPanel(); this.createSpecialDataPanel(); }, saveGlobalSettings() { try { localStorage.setItem(CONFIG.settingsStorageKey, JSON.stringify(this.globalSettings)); } catch (e) { } }, /** * 应用主题 * @param {string} themeName */ applyTheme(themeName) { const themeObj = CONFIG.themes[themeName] || CONFIG.themes.light; const rootStyle = document.documentElement.style; // 将 themeObj 的 key => 转成 --xxx Object.entries(themeObj).forEach(([k, v]) => { rootStyle.setProperty(`--${k.replace(/([A-Z])/g, '-$1').toLowerCase()}`, v); }); this.globalSettings.currentTheme = themeName; this.saveGlobalSettings(); // 更新所有已存在面板的背景 const panels = GlobalPanels.getAllPanels(); for (const p of panels) { if (typeof p.updatePanelBackgroundByTheme === 'function') { p.updatePanelBackgroundByTheme(); } } UILogger.logMessage(`[UIManager] 已切换主题 => ${themeName}`, 'info'); }, /** * 将字号、间距、阴影等写入CSS变量 */ applyDimensionsAndEffects() { const rootStyle = document.documentElement.style; // 字号 Object.entries(CONFIG.fontSizes).forEach(([key, val]) => { rootStyle.setProperty(`--font-size-${key}`, val); }); // 图标尺寸 Object.entries(CONFIG.iconSizes).forEach(([key, val]) => { rootStyle.setProperty(`--button-size-${key}`, val); }); // 面板特效 rootStyle.setProperty('--border-radius', CONFIG.panelEffects.borderRadius); rootStyle.setProperty('--box-shadow-default', CONFIG.panelEffects.defaultBoxShadow); rootStyle.setProperty('--box-shadow-hover', CONFIG.panelEffects.hoverBoxShadow); rootStyle.setProperty('--titlebar-bottom-border', CONFIG.panelEffects.titlebarBottomBorder); rootStyle.setProperty('--minimized-height', CONFIG.panelEffects.minimizedHeight); // 额外布局/间距 rootStyle.setProperty('--drag-handle-size', CONFIG.layout.dragHandleSize); rootStyle.setProperty('--drag-handle-margin', CONFIG.layout.dragHandleMargin); rootStyle.setProperty('--inline-confirm-padding', CONFIG.layout.inlineConfirmPadding); rootStyle.setProperty('--inline-confirm-button-padding', CONFIG.layout.inlineConfirmButtonPadding); rootStyle.setProperty('--progress-bar-height', CONFIG.layout.progressBarHeight); }, /** * 创建一个主题切换按钮 */ createThemeToggleButton() { return BaseFloatingPanel.createPanelButton('THEME_TOGGLE', () => { const newTheme = (this.globalSettings.currentTheme === 'light') ? 'dark' : 'light'; this.applyTheme(newTheme); }); }, createJsonPanel() { const initPos = CONFIG.initialPanels.jsonPanel; this.jsonPanel = new BaseFloatingPanel({ id: 'json-panel-container', title: 'JSON 抓取器', defaultLeft: initPos.left, defaultTop: initPos.top, defaultWidth: initPos.width, defaultHeight: initPos.height, reopenBtnText: '打开JSON抓取器', reopenBtnTop: '10px', allowResize: true, onClose: () => UILogger.logMessage('JSON面板已关闭', 'info'), onMinimize: () => UILogger.logMessage('JSON面板已最小化', 'info'), onRestore: () => UILogger.logMessage('JSON面板已还原', 'info'), onFocus: () => UILogger.logMessage('JSON面板获得焦点', 'debug'), onOpen: () => UILogger.logMessage('JSON面板创建完成', 'debug'), // 示例:可开启双击标题栏最大化/还原 doubleClickTitleToToggleMaximize: true }); // 主题切换按钮 const btnTheme = this.createThemeToggleButton(); // 分类显示切换 const btnToggleCat = BaseFloatingPanel.createPanelButton('TOGGLE_CAT', () => { this.globalSettings.useCategories = !this.globalSettings.useCategories; this.saveGlobalSettings(); this.rebuildJsonPanelContent(); UILogger.logMessage(`切换分类显示: ${this.globalSettings.useCategories}`, 'info'); }); // 将两个新增按钮插到最小化按钮之前 this.jsonPanel.titlebar.insertBefore(btnToggleCat, this.jsonPanel.btnMinimize); this.jsonPanel.titlebar.insertBefore(btnTheme, btnToggleCat); this.rebuildJsonPanelContent(); }, rebuildJsonPanelContent() { const contentWrap = this.jsonPanel.contentEl; contentWrap.innerHTML = ''; // 搜索栏 const searchWrap = document.createElement('div'); searchWrap.className = 'json-panel-search-wrap'; const lbl = document.createElement('label'); lbl.textContent = '搜索:'; const inp = document.createElement('input'); inp.type = 'text'; inp.className = 'json-panel-search-input'; inp.placeholder = '按URL/filename过滤...'; inp.value = this.currentSearchText; inp.addEventListener('input', () => { this.currentSearchText = inp.value.trim().toLowerCase(); this.updateLists(); }); searchWrap.appendChild(lbl); searchWrap.appendChild(inp); contentWrap.appendChild(searchWrap); // 判断分类或不分类 if (this.globalSettings.useCategories) { this.buildCategory('星标', 'star', contentWrap); this.buildCategory('Backend API', 'backend', contentWrap); this.buildCategory('Public API', 'public', contentWrap); this.buildCategory('API', 'api', contentWrap); this.buildCategory('其他', 'other', contentWrap); } else { this.buildCategory('所有请求', 'all', contentWrap); } this.updateLists(); }, buildCategory(title, catKey, parent) { const wrapper = document.createElement('div'); wrapper.className = 'json-panel-category'; wrapper.style.margin = CONFIG.layout.categoryMargin; const header = document.createElement('div'); header.className = 'json-panel-category-header'; header.style.padding = CONFIG.layout.categoryHeaderPadding; const titleSpan = document.createElement('span'); titleSpan.className = 'title'; titleSpan.textContent = title; const btnsWrap = document.createElement('div'); // 批量下载 const btnDownload = BaseFloatingPanel.createPanelButton('DOWNLOAD_ALL', () => { const list = this.getRequestsByCategory(catKey); if (!list.length) { UILogger.logMessage(`【${title}】无可下载数据`, 'warn'); return; } list.forEach(item => this.downloadSingle(item)); UILogger.logMessage(`批量下载完成,分类【${title}】共${list.length}个`, 'info'); }); btnDownload.title = `批量下载: ${title}`; // 清空此分类 const btnClear = BaseFloatingPanel.createPanelButton('CLEAR_CATEGORY', () => { inlineConfirm(`确定要清空分类「${title}」吗?此操作不可恢复。`, () => { if (catKey === 'all') { RequestInterceptor.capturedRequests = []; } else { this.removeRequestsByCategory(catKey); } this.updateLists(); UILogger.logMessage(`已清空分类: ${title}`, 'warn'); }); }); btnClear.title = `清空: ${title}`; // 按名称排序 let sortNameAsc = true; const btnSortName = document.createElement('button'); btnSortName.className = 'floating-panel-btn'; btnSortName.textContent = BUTTON_MAP.SORT_ASC.icon; btnSortName.title = `按名称排序 - ${title}`; btnSortName.addEventListener('click', () => { this.sortCategory(catKey, 'name', sortNameAsc); sortNameAsc = !sortNameAsc; btnSortName.textContent = sortNameAsc ? BUTTON_MAP.SORT_ASC.icon : BUTTON_MAP.SORT_DESC.icon; }); // 按大小排序 let sortSizeAsc = true; const btnSortSize = document.createElement('button'); btnSortSize.className = 'floating-panel-btn'; btnSortSize.textContent = BUTTON_MAP.SORT_ASC.icon; btnSortSize.title = `按大小排序 - ${title}`; btnSortSize.addEventListener('click', () => { this.sortCategory(catKey, 'size', sortSizeAsc); sortSizeAsc = !sortSizeAsc; btnSortSize.textContent = sortSizeAsc ? BUTTON_MAP.SORT_ASC.icon : BUTTON_MAP.SORT_DESC.icon; }); btnsWrap.appendChild(btnDownload); btnsWrap.appendChild(btnClear); btnsWrap.appendChild(btnSortName); btnsWrap.appendChild(btnSortSize); header.appendChild(titleSpan); header.appendChild(btnsWrap); const listEl = document.createElement('ul'); listEl.className = 'json-panel-list'; wrapper.appendChild(header); wrapper.appendChild(listEl); parent.appendChild(wrapper); // 保存引用 switch (catKey) { case 'star': this.starListEl = listEl; break; case 'backend': this.backendListEl = listEl; break; case 'public': this.publicListEl = listEl; break; case 'api': this.apiListEl = listEl; break; case 'other': this.otherListEl = listEl; break; case 'all': this.singleListEl = listEl; break; } }, updateLists() { if (!this.jsonPanel) return; if (this.globalSettings.useCategories) { if (this.starListEl) { this.starListEl.innerHTML = ''; this.getRequestsByCategory('star').forEach(it => { this.starListEl.appendChild(this.createRequestItem(it)); }); } if (this.backendListEl) { this.backendListEl.innerHTML = ''; this.getRequestsByCategory('backend').forEach(it => { this.backendListEl.appendChild(this.createRequestItem(it)); }); } if (this.publicListEl) { this.publicListEl.innerHTML = ''; this.getRequestsByCategory('public').forEach(it => { this.publicListEl.appendChild(this.createRequestItem(it)); }); } if (this.apiListEl) { this.apiListEl.innerHTML = ''; this.getRequestsByCategory('api').forEach(it => { this.apiListEl.appendChild(this.createRequestItem(it)); }); } if (this.otherListEl) { this.otherListEl.innerHTML = ''; this.getRequestsByCategory('other').forEach(it => { this.otherListEl.appendChild(this.createRequestItem(it)); }); } } else { if (this.singleListEl) { this.singleListEl.innerHTML = ''; this.getRequestsByCategory('all').forEach(it => { this.singleListEl.appendChild(this.createRequestItem(it)); }); } } }, getRequestsByCategory(cat) { const arr = RequestInterceptor.capturedRequests; if (cat === 'all') { return this.filterBySearch(arr); } else { return this.filterBySearch(arr.filter(it => it.category === cat)); } }, filterBySearch(arr) { if (!this.currentSearchText) return arr; return arr.filter(it => { const urlLower = (it.url || '').toLowerCase(); const fileLower = (it.filename || '').toLowerCase(); return (urlLower.includes(this.currentSearchText) || fileLower.includes(this.currentSearchText)); }); }, removeRequestsByCategory(cat) { RequestInterceptor.capturedRequests = RequestInterceptor.capturedRequests.filter(it => it.category !== cat); }, sortCategory(cat, by, asc) { let arr = (cat === 'all') ? RequestInterceptor.capturedRequests : this.getRequestsByCategory(cat); if (by === 'name') { arr.sort((a, b) => asc ? a.filename.localeCompare(b.filename) : b.filename.localeCompare(a.filename)); } else if (by === 'size') { arr.sort((a, b) => { const sa = parseFloat(a.sizeKB); const sb = parseFloat(b.sizeKB); return asc ? (sa - sb) : (sb - sa); }); } if (cat !== 'all') { // 移除此分类旧数据,再插入排好序的新数据 this.removeRequestsByCategory(cat); arr.forEach(it => RequestInterceptor.capturedRequests.push(it)); } else { RequestInterceptor.capturedRequests = arr; } this.updateLists(); }, createRequestItem(item) { const li = document.createElement('li'); li.className = 'json-panel-item'; li.style.padding = CONFIG.layout.itemPadding; // 复制 const btnCopy = document.createElement('span'); btnCopy.className = 'icon'; btnCopy.textContent = BUTTON_MAP.COPY_JSON.icon; btnCopy.title = BUTTON_MAP.COPY_JSON.title; btnCopy.addEventListener('click', () => { copyText(item.content); UILogger.logMessage('复制JSON: ' + item.filename, 'info'); }); // 下载 const btnDownload = document.createElement('span'); btnDownload.className = 'icon'; btnDownload.textContent = BUTTON_MAP.DOWNLOAD_JSON.icon; btnDownload.title = BUTTON_MAP.DOWNLOAD_JSON.title; btnDownload.addEventListener('click', () => { this.downloadSingle(item); }); // 预览 const btnPreview = document.createElement('span'); btnPreview.className = 'icon'; btnPreview.textContent = BUTTON_MAP.PREVIEW_JSON.icon; btnPreview.title = BUTTON_MAP.PREVIEW_JSON.title; btnPreview.addEventListener('click', () => { this.previewJson(item); }); // 删除 const btnRemoveItem = document.createElement('span'); btnRemoveItem.className = 'icon'; btnRemoveItem.textContent = BUTTON_MAP.REMOVE_ITEM.icon; btnRemoveItem.title = BUTTON_MAP.REMOVE_ITEM.title; btnRemoveItem.addEventListener('click', () => { inlineConfirm(`确定删除此记录?\n\nURL: ${item.url}`, () => { const idx = RequestInterceptor.capturedRequests.indexOf(item); if (idx >= 0) { RequestInterceptor.capturedRequests.splice(idx, 1); UILogger.logMessage(`删除抓取记录: ${item.filename} (URL: ${item.url})`, 'warn'); this.updateLists(); } }); }); const fileSpan = document.createElement('span'); fileSpan.className = 'filename-span'; fileSpan.textContent = item.filename; const urlSpan = document.createElement('span'); urlSpan.className = 'url-span'; urlSpan.textContent = item.url; urlSpan.title = item.url; const sizeSpan = document.createElement('span'); sizeSpan.className = 'size-span'; sizeSpan.textContent = item.sizeKB + 'KB'; li.appendChild(btnCopy); li.appendChild(btnDownload); li.appendChild(btnPreview); li.appendChild(btnRemoveItem); li.appendChild(fileSpan); li.appendChild(urlSpan); li.appendChild(sizeSpan); return li; }, previewJson(item) { if (!item || !item.content) { UILogger.logMessage('预览失败: JSON为空', 'warn'); return; } BaseFloatingPanel.openPreviewPanel(item.filename, item.content); }, downloadSingle(item) { if (!item || !item.content) { UILogger.logMessage('下载失败: JSON为空', 'warn'); return; } let fn = item.filename || 'download'; if (!fn.endsWith('.json')) fn += '.json'; downloadFile(item.content, fn); UILogger.logMessage(`下载JSON: ${fn}`, 'info'); }, refreshJsonPanelTitle() { if (!this.jsonPanel) return; let t = 'JSON 抓取器'; if (CONFIG.showPoWDifficulty && PoWParser.currentDifficulty) { t += ` (PoW难度: ${PoWParser.currentDifficulty})`; } this.jsonPanel.setTitle(t); }, createSpecialDataPanel() { const initPos = CONFIG.initialPanels.specPanel; this.specialDataPanel = new BaseFloatingPanel({ id: 'special-data-panel-container', title: '特殊数据解析', defaultLeft: initPos.left, defaultTop: initPos.top, defaultWidth: initPos.width, defaultHeight: initPos.height, reopenBtnText: '打开“特殊解析”面板', reopenBtnTop: '130px', allowResize: true, onClose: () => UILogger.logMessage('特殊数据解析面板已关闭', 'info'), onMinimize: () => UILogger.logMessage('特殊数据解析面板已最小化', 'info'), onRestore: () => UILogger.logMessage('特殊数据解析面板已还原', 'info'), onFocus: () => UILogger.logMessage('特殊数据解析面板获得焦点', 'debug'), onOpen: () => UILogger.logMessage('特殊数据解析面板创建完成', 'debug') }); // 工具栏按钮:清空、导出CSV、折叠/展开全部 const btnClear = BaseFloatingPanel.createPanelButton('TRASH', () => { inlineConfirm('确定清空全部解析数据吗?此操作不可恢复。', () => { SpecialDataParser.claudeConvData.length = 0; SpecialDataParser.chatgptConvData.length = 0; SpecialDataParser.chatgptTasksData.length = 0; this.updateSpecialDataPanel(); UILogger.logMessage('已清空特殊数据解析', 'warn'); }); }); btnClear.title = '清空所有解析数据(Claude/ChatGPT)'; const btnCSV = BaseFloatingPanel.createPanelButton('TO_CSV', () => this.downloadSpecialDataAsCSV()); btnCSV.title = '导出所有解析数据为CSV'; const btnFoldAll = BaseFloatingPanel.createPanelButton('FOLD_ALL', () => this.foldAllCategories(true)); const btnUnfoldAll = BaseFloatingPanel.createPanelButton('UNFOLD_ALL', () => this.foldAllCategories(false)); const fragBar = document.createDocumentFragment(); fragBar.appendChild(btnClear); fragBar.appendChild(btnCSV); fragBar.appendChild(btnFoldAll); fragBar.appendChild(btnUnfoldAll); this.specialDataPanel.titlebar.insertBefore(fragBar, this.specialDataPanel.btnMinimize); this.buildSpecialDataPanelUI(); this.updateSpecialDataPanel(); }, buildSpecialDataPanelUI() { const wrap = this.specialDataPanel.contentEl; wrap.innerHTML = ''; // Claude分类 this.claudeCat = this.createFoldableCategory('Claude对话'); wrap.appendChild(this.claudeCat.wrapper); const topBar = document.createElement('div'); topBar.style.display = 'inline-flex'; topBar.style.gap = '6px'; topBar.style.marginLeft = 'auto'; CONFIG.claudeBatchButtons.forEach(cfg => { if (!cfg.enabled) return; const btn = document.createElement('button'); btn.className = 'floating-panel-btn'; btn.textContent = cfg.icon || cfg.label; btn.title = `下载${cfg.days === Infinity ? '全部' : '最近' + cfg.days + '天'}的Claude对话`; btn.addEventListener('click', () => { if (cfg.days === Infinity) { this.batchDownloadClaude(SpecialDataParser.claudeConvData, cfg.label); } else { this.batchDownloadClaudeWithinDays(cfg.days); } }); topBar.appendChild(btn); }); this.claudeCat.header.appendChild(topBar); // 进度条容器 const progressWrap = document.createElement('div'); progressWrap.className = 'claude-progress-wrap'; progressWrap.style.display = 'none'; const progressBar = document.createElement('div'); progressBar.className = 'claude-progress-bar'; const progressText = document.createElement('div'); progressText.className = 'claude-progress-text'; progressText.textContent = ''; progressWrap.appendChild(progressBar); progressWrap.appendChild(progressText); this.claudeCat.content.appendChild(progressWrap); this.claudeProgressWrap = progressWrap; this.claudeProgressBar = progressBar; this.claudeProgressText = progressText; const claudeUl = document.createElement('ul'); claudeUl.className = 'special-data-list'; this.claudeCat.content.appendChild(claudeUl); this.claudeListEl = claudeUl; // ChatGPT对话分类 this.chatgptCat = this.createFoldableCategory('ChatGPT对话'); wrap.appendChild(this.chatgptCat.wrapper); const chatgptUl = document.createElement('ul'); chatgptUl.className = 'special-data-list'; this.chatgptCat.content.appendChild(chatgptUl); this.chatgptListEl = chatgptUl; // ChatGPT任务分类 this.chatgptTaskCat = this.createFoldableCategory('ChatGPT任务'); wrap.appendChild(this.chatgptTaskCat.wrapper); const taskUl = document.createElement('ul'); taskUl.className = 'special-data-list'; this.chatgptTaskCat.content.appendChild(taskUl); this.chatgptTasksListEl = taskUl; }, createFoldableCategory(title) { const wrapper = document.createElement('div'); wrapper.className = 'special-data-category'; wrapper.style.margin = CONFIG.layout.categoryMargin; const header = document.createElement('div'); header.className = 'special-data-category-header'; header.style.padding = CONFIG.layout.categoryHeaderPadding; const foldIcon = document.createElement('span'); foldIcon.textContent = BUTTON_MAP.UNFOLD_ALL.icon; // 默认展开图标 foldIcon.style.marginRight = '4px'; foldIcon.style.cursor = 'pointer'; foldIcon.style.color = 'var(--fold-icon-color)'; const titleSpan = document.createElement('span'); titleSpan.className = 'title'; titleSpan.textContent = title; header.appendChild(foldIcon); header.appendChild(titleSpan); const content = document.createElement('div'); content.style.display = 'block'; wrapper.appendChild(header); wrapper.appendChild(content); let folded = false; foldIcon.addEventListener('click', () => { folded = !folded; foldIcon.textContent = folded ? BUTTON_MAP.FOLD_ALL.icon : BUTTON_MAP.UNFOLD_ALL.icon; content.style.display = folded ? 'none' : 'block'; }); return {wrapper, header, content, foldIcon, folded}; }, foldAllCategories(fold) { [this.claudeCat, this.chatgptCat, this.chatgptTaskCat].forEach(catObj => { if (catObj) { catObj.folded = fold; catObj.foldIcon.textContent = fold ? BUTTON_MAP.FOLD_ALL.icon : BUTTON_MAP.UNFOLD_ALL.icon; catObj.content.style.display = fold ? 'none' : 'block'; } }); }, updateSpecialDataPanel() { // Claude if (this.claudeListEl) { this.claudeListEl.innerHTML = ''; SpecialDataParser.claudeConvData.forEach(item => { const li = document.createElement('li'); li.className = 'special-data-list-item'; li.style.padding = CONFIG.layout.itemPadding; const line1 = document.createElement('div'); line1.className = 'special-data-item-line'; line1.style.display = 'flex'; line1.style.justifyContent = 'space-between'; line1.style.alignItems = 'center'; line1.style.marginBottom = '4px'; const leftSpan = document.createElement('span'); leftSpan.innerHTML = `name: ${item.name || ''}`; line1.appendChild(leftSpan); if (item.convUrl) { const dlIcon = document.createElement('span'); dlIcon.textContent = BUTTON_MAP.DOWNLOAD_ALL.icon; dlIcon.style.cursor = 'pointer'; dlIcon.title = '下载此对话'; dlIcon.addEventListener('click', () => { SpecialDataParser.downloadClaudeConversation(item); }); line1.appendChild(dlIcon); } li.appendChild(line1); const line2 = document.createElement('div'); line2.className = 'special-data-item-line'; line2.innerHTML = `uuid: ${item.uuid || ''}`; const line3 = document.createElement('div'); line3.className = 'special-data-item-line'; line3.innerHTML = `updated_at: ${item.updated_at_shanghai || ''}`; li.appendChild(line2); li.appendChild(line3); this.claudeListEl.appendChild(li); }); } // ChatGPT对话 if (this.chatgptListEl) { this.chatgptListEl.innerHTML = ''; SpecialDataParser.chatgptConvData.forEach(item => { const li = document.createElement('li'); li.className = 'special-data-list-item'; li.style.padding = CONFIG.layout.itemPadding; const line1 = document.createElement('div'); line1.className = 'special-data-item-line'; line1.style.display = 'flex'; line1.style.justifyContent = 'space-between'; line1.style.alignItems = 'center'; line1.style.marginBottom = '4px'; const leftSpan = document.createElement('span'); leftSpan.innerHTML = `title: ${item.title || ''}`; line1.appendChild(leftSpan); if (item.convUrl) { const dlIcon = document.createElement('span'); dlIcon.textContent = BUTTON_MAP.DOWNLOAD_ALL.icon; dlIcon.style.cursor = 'pointer'; dlIcon.title = '下载此对话'; dlIcon.addEventListener('click', () => { SpecialDataParser.downloadChatGPTConversation(item); }); line1.appendChild(dlIcon); } li.appendChild(line1); const line2 = document.createElement('div'); line2.className = 'special-data-item-line'; line2.innerHTML = `id: ${item.id || ''}`; const line3 = document.createElement('div'); line3.className = 'special-data-item-line'; line3.innerHTML = `update_time: ${item.update_time_shanghai || ''}`; li.appendChild(line2); li.appendChild(line3); this.chatgptListEl.appendChild(li); }); } // ChatGPT任务 if (this.chatgptTasksListEl) { this.chatgptTasksListEl.innerHTML = ''; SpecialDataParser.chatgptTasksData.forEach(task => { const li = document.createElement('li'); li.className = 'special-data-list-item'; li.style.padding = CONFIG.layout.itemPadding; const line1 = document.createElement('div'); line1.className = 'special-data-item-line'; line1.style.display = 'flex'; line1.style.justifyContent = 'space-between'; line1.style.alignItems = 'center'; line1.style.marginBottom = '4px'; const leftSpan = document.createElement('span'); leftSpan.innerHTML = `title: ${task.title || ''}`; line1.appendChild(leftSpan); li.appendChild(line1); const line2 = document.createElement('div'); line2.className = 'special-data-item-line'; line2.innerHTML = `task_id: ${task.task_id || ''}`; const line3 = document.createElement('div'); line3.className = 'special-data-item-line'; line3.innerHTML = `updated_at: ${task.updated_at_shanghai || ''}`; const line4 = document.createElement('div'); line4.className = 'special-data-item-line'; line4.innerHTML = `original_conversation_id: ${task.original_conversation_id || ''}`; const line5 = document.createElement('div'); line5.className = 'special-data-item-line'; line5.innerHTML = `conversation_id: ${task.conversation_id || ''}`; li.appendChild(line2); li.appendChild(line3); li.appendChild(line4); li.appendChild(line5); this.chatgptTasksListEl.appendChild(li); }); } }, downloadSpecialDataAsCSV() { const domain = location.hostname.replace(/[\\/:*?"<>|]/g, '_') || 'site'; const now = new Date(); const y = now.getFullYear(); const M = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); const fileTime = `${y}${M}${d}-${hh}${mm}${ss}`; const filename = `special-data-${domain}-${fileTime}.csv`; let lines = ['Type,TitleOrName,ID/ConvID,UpdateTime,TaskID,OriginalConvID']; // Claude SpecialDataParser.claudeConvData.forEach(it => { const type = 'Claude'; const name = (it.name || '').replace(/"/g, '""'); const id = (it.uuid || '').replace(/"/g, '""'); const t = (it.updated_at_shanghai || '').replace(/"/g, '""'); lines.push(`"${type}","${name}","${id}","${t}","",""`); }); // ChatGPT SpecialDataParser.chatgptConvData.forEach(it => { const type = 'ChatGPT'; const name = (it.title || '').replace(/"/g, '""'); const id = (it.id || '').replace(/"/g, '""'); const t = (it.update_time_shanghai || '').replace(/"/g, '""'); lines.push(`"${type}","${name}","${id}","${t}","",""`); }); // ChatGPT任务 SpecialDataParser.chatgptTasksData.forEach(it => { const type = 'ChatGPT-Task'; const name = (it.title || '').replace(/"/g, '""'); const cid = (it.conversation_id || '').replace(/"/g, '""'); const t = (it.updated_at_shanghai || '').replace(/"/g, '""'); const tk = (it.task_id || '').replace(/"/g, '""'); const org = (it.original_conversation_id || '').replace(/"/g, '""'); lines.push(`"${type}","${name}","${cid}","${t}","${tk}","${org}"`); }); const csvText = lines.join('\r\n'); downloadFile(csvText, filename, 'text/csv'); UILogger.logMessage(`特殊数据CSV已下载: ${filename}`, 'info'); }, showClaudeProgressBar(show) { if (!this.claudeProgressWrap) return; this.claudeProgressWrap.style.display = show ? 'block' : 'none'; if (!show) { this.claudeProgressBar.style.width = '0%'; this.claudeProgressText.textContent = ''; } }, updateClaudeProgress(current, total, label, errorMsg) { if (!this.claudeProgressBar || !this.claudeProgressText) return; const pct = Math.floor((current / total) * 100); this.claudeProgressBar.style.width = pct + '%'; let text = `下载进度:${current}/${total}(${pct}%)`; if (errorMsg) { text += `\n错误: ${errorMsg}`; } this.claudeProgressText.textContent = text; }, batchDownloadClaude(list, label) { if (!list || !list.length) { UILogger.logMessage(`Claude批量下载【${label}】无数据`, 'warn'); return; } UILogger.logMessage(`开始批量下载Claude对话【${label}】,共${list.length}条`, 'info'); this.setFoldState(this.claudeCat, false); this.showClaudeProgressBar(true); this.updateClaudeProgress(0, list.length, label); const dq = new DownloadQueue({ maxConcurrent: CONFIG.downloadQueueOptions.maxConcurrent, maxRetry: CONFIG.downloadQueueOptions.maxRetry, retryDelay: CONFIG.downloadQueueOptions.retryDelay }); list.forEach(item => { dq.addTask(item, async () => { await SpecialDataParser.downloadClaudeConversation(item); }); }); dq.onProgress = (doneCount, totalCount, task) => { let errMsg = null; if (task.error) { errMsg = task.error.message || String(task.error); UILogger.logMessage(`[Claude下载进度] 出错: ${errMsg}`, 'error'); } this.updateClaudeProgress(doneCount, totalCount, label, errMsg); }; dq.onComplete = (successCount, failCount) => { this.showClaudeProgressBar(false); const msg = `Claude批量下载【${label}】完成:成功${successCount},失败${failCount}`; UILogger.logMessage(msg, failCount > 0 ? 'warn' : 'info'); }; dq.start(); }, batchDownloadClaudeWithinDays(days) { const now = new Date(); const filtered = SpecialDataParser.claudeConvData.filter(item => { if (!item.updated_at_shanghai) return false; const dt = new Date(item.updated_at_shanghai); if (isNaN(dt.getTime())) return false; const diffDays = (now - dt) / (1000 * 60 * 60 * 24); return diffDays <= days; }); this.batchDownloadClaude(filtered, `最近${days}天`); }, setFoldState(catObj, fold) { if (!catObj) return; catObj.folded = fold; catObj.foldIcon.textContent = fold ? BUTTON_MAP.FOLD_ALL.icon : BUTTON_MAP.UNFOLD_ALL.icon; catObj.content.style.display = fold ? 'none' : 'block'; } }; /************************************************************************ * 10. 主入口(main) & 样式注入 ************************************************************************/ function findStarUuid() { // 如果URL中含 /c/xxxxxx 这样的UUID,就提取出来用于特殊标记 const m = /\/c\/([0-9a-fA-F-]+)/.exec(location.href); if (m) RequestInterceptor.starUuid = m[1]; } function main() { try { findStarUuid(); UILogger.init(); UIManager.init(); RequestInterceptor.init(); UILogger.logMessage('脚本已启动 - 面板已生成!', 'info'); } catch (err) { logErrorWithStack(err, 'main'); } } function waitForBody() { if (document.body) { main(); } else { requestAnimationFrame(waitForBody); } } waitForBody(); // 注入CSS(所有颜色/字号/尺寸都从CONFIG里映射为CSS变量) const cssText = ` /* =========================== 行内确认(InlineConfirm) =========================== */ .inline-confirm-container { position: fixed; right: 16px; bottom: 16px; z-index: 999999999; background: var(--inline-confirm-bg); color: var(--inline-confirm-text); border: 1px solid var(--inline-confirm-border); padding: var(--inline-confirm-padding); border-radius: 6px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.6); display: flex; align-items: center; gap: 8px; font-size: var(--font-size-inline-confirm); animation: fadeIn 0.2s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .inline-confirm-text { margin-right: 6px; } .inline-confirm-btn { border: 1px solid #ccc; background: var(--inline-confirm-btn-bg); color: inherit; border-radius: 4px; cursor: pointer; font-size: var(--font-size-inline-confirm); padding: var(--inline-confirm-button-padding); transition: background 0.2s ease; } .inline-confirm-btn:hover { background: var(--inline-confirm-btn-hover-bg); } .inline-confirm-yes { background: var(--inline-confirm-yes-bg); color: var(--inline-confirm-yes-text); margin-left: 6px; } .inline-confirm-no { background: var(--inline-confirm-no-bg); color: var(--inline-confirm-no-text); } /* ============================= 浮动面板基类 ============================= */ .floating-panel-container { position: fixed; backdrop-filter: blur(4px); background: var(--panel-content-bg); border: 1px solid var(--panel-border-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow-default); display: flex; flex-direction: column; resize: both; overflow: hidden; transition: box-shadow 0.2s ease; z-index: 999999; font-family: system-ui, sans-serif; } .floating-panel-container:hover { box-shadow: var(--box-shadow-hover); } .floating-panel-container.minimized { overflow: hidden; resize: none; height: var(--minimized-height) !important; } .floating-panel-titlebar { flex-shrink: 0; background: var(--panel-title-bg-gradient); height: 36px; display: flex; align-items: center; padding: 0 4px; cursor: default; border-top-left-radius: var(--border-radius); border-top-right-radius: var(--border-radius); border-bottom: 1px solid var(--titlebar-bottom-border); } .floating-panel-drag-handle { width: var(--drag-handle-size); height: var(--drag-handle-size); margin: var(--drag-handle-margin); background-color: var(--panel-handle-color); border-radius: 4px; cursor: move; box-shadow: var(--drag-handle-inner-shadow); } .floating-panel-title { flex: 1; font-weight: 600; padding-left: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: text; font-size: var(--font-size-title); color: var(--panel-title-text-color); } .floating-panel-btn { cursor: pointer; border: none; background: transparent; margin: 0 1px; padding: 0 5px; border-radius: 4px; transition: background 0.2s ease; font-size: var(--button-size-titlebar); color: var(--panel-btn-text-color); } .floating-panel-btn:hover { background: var(--panel-btn-hover-bg); } /* 让最小化 & 关闭按钮真正跟随主题,不要被覆盖 */ .floating-panel-btn.minimize-btn { color: var(--panel-minimize-btn-color) !important; } .floating-panel-btn.close-btn { color: var(--panel-close-btn-color) !important; } .floating-reopen-btn { display: none; position: fixed; left: 10px; border: 1px solid var(--floating-reopen-btn-border); border-radius: 4px; padding: 6px 12px; cursor: pointer; z-index: 999999999; color: var(--panel-btn-text-color); background: var(--panel-reopen-btn-bg); font-size: var(--font-size-content); } .floating-panel-content { flex: 1; overflow: auto; font-size: var(--font-size-content); color: var(--panel-btn-text-color); } /* =============================== 日志面板 =============================== */ .log-panel-list { list-style: none; margin: 0; padding: 0; font-family: monospace; font-size: var(--font-size-log); line-height: 1.2; color: var(--panel-log-font-color); white-space: pre; } .log-panel-list.wrap-lines { white-space: pre-wrap; word-wrap: break-word; } .log-line { margin: 2px 0; } /* ============================== JSON面板搜索 ============================== */ .json-panel-search-wrap { margin: 4px; display: flex; align-items: center; } .json-panel-search-wrap label { margin-right: 4px; color: var(--search-label-color); } .json-panel-search-input { flex: 1; border: 1px solid var(--search-input-border); border-radius: 4px; padding: 4px 6px; font-size: var(--font-size-content); background: transparent; color: var(--panel-btn-text-color); } /* ============================== JSON分类 ============================== */ .json-panel-category { border: 1px solid var(--category-border-color); border-radius: 6px; background: transparent; padding-bottom: 4px; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); } .json-panel-category-header { display: flex; align-items: center; justify-content: space-between; background: var(--category-header-bg); border-bottom: 1px solid var(--category-border-color); border-top-left-radius: 6px; border-top-right-radius: 6px; } .json-panel-category-header .title { font-weight: bold; margin-right: 8px; color: var(--category-title-color); font-size: var(--font-size-category-title); } .json-panel-list { list-style: none; margin: 0; padding: 0; } .json-panel-item { display: flex; align-items: center; border-bottom: 1px solid var(--item-divider-color); font-size: var(--font-size-category-item); color: var(--panel-btn-text-color); } .json-panel-item:hover { background: var(--item-hover-bg); } .json-panel-item .icon { cursor: pointer; margin-right: 6px; font-size: var(--button-size-category-item); color: var(--panel-btn-text-color); } .filename-span { margin-right: 6px; font-weight: bold; } .url-span { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-right: 6px; color: var(--json-url-color); } .size-span { color: var(--json-size-color); } /* =============================== JSON预览 =============================== */ .json-preview-content { background: rgba(246, 248, 250, 0.2); padding: 8px; overflow: auto; flex: 1; } .json-preview { font-family: Consolas, Monaco, monospace; font-size: var(--font-size-content); white-space: pre; line-height: 1.4em; color: #ccc; } .json-preview .string { color: var(--highlight-string-color); } .json-preview .number { color: var(--highlight-number-color); } .json-preview .boolean { color: var(--highlight-boolean-color); } .json-preview .null { color: var(--highlight-null-color); } .json-preview .key { color: var(--highlight-key-color); } /* ============================ 特殊数据面板 ============================ */ .special-data-category { border: 1px solid var(--category-border-color); border-radius: 6px; background: transparent; padding-bottom: 4px; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); } .special-data-category-header { display: flex; align-items: center; background: var(--category-header-bg); border-bottom: 1px solid var(--category-border-color); border-top-left-radius: 6px; border-top-right-radius: 6px; } .special-data-category-header .title { font-weight: bold; margin-right: 6px; color: var(--category-title-color); font-size: var(--font-size-category-title); } .special-data-list { list-style: none; margin: 0; padding: 0; } .special-data-list-item { display: flex; flex-direction: column; border-bottom: 1px solid var(--item-divider-color); font-size: var(--font-size-category-item); color: var(--panel-btn-text-color); } .special-data-list-item:hover { background: var(--item-hover-bg); } .special-data-item-line { margin: 2px 0; font-size: var(--font-size-category-item); } /* ============================ Claude进度条 ============================ */ .claude-progress-wrap { margin: 8px; border: 1px solid var(--panel-border-color); border-radius: 4px; height: var(--progress-bar-height); position: relative; background: var(--progress-wrap-bg); overflow: hidden; } .claude-progress-bar { position: absolute; left: 0; top: 0; width: 0%; height: 100%; background: var(--progress-bar-bg); transition: width 0.2s ease; } .claude-progress-text { position: absolute; left: 0; top: 0; width: 100%; height: 100%; text-align: center; line-height: var(--progress-bar-height); font-size: var(--font-size-content); color: var(--progress-bar-text-color); pointer-events: none; white-space: pre-wrap; } `; const styleEl = document.createElement('style'); styleEl.textContent = cssText; document.head.appendChild(styleEl); })();