// ==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
// ==/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 = `
<div class="inline-confirm-text">${question}</div>
<button class="inline-confirm-btn inline-confirm-yes" title="${BUTTON_MAP.CONFIRM_CHECK.title}">${BUTTON_MAP.CONFIRM_CHECK.icon}</button>
<button class="inline-confirm-btn inline-confirm-no" title="${BUTTON_MAP.CONFIRM_CANCEL.title}">${BUTTON_MAP.CONFIRM_CANCEL.icon}</button>
`;
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, '<')
.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 `<span class="${cls}">${match}</span>`;
}
);
} 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 = `<div class="json-preview">${highlightJson(pretty)}</div>`;
ephemeralPanel.contentEl.innerHTML = `
<div class="json-preview-content" style="flex:1;overflow:auto;padding:8px;">${html}</div>
`;
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 = `<strong style="color:var(--special-title-color);">name:</strong>
<span style="color:var(--special-title-color);">${item.name || ''}</span>`;
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 = `<strong style="color:var(--special-uuid-color);">uuid:</strong>
<span style="color:var(--special-uuid-color);">${item.uuid || ''}</span>`;
const line3 = document.createElement('div');
line3.className = 'special-data-item-line';
line3.innerHTML = `<strong style="color:var(--special-update-color);">updated_at:</strong>
<span style="color:var(--special-update-color);">${item.updated_at_shanghai || ''}</span>`;
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 = `<strong style="color:var(--special-title-color);">title:</strong>
<span style="color:var(--special-title-color);">${item.title || ''}</span>`;
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 = `<strong style="color:var(--special-uuid-color);">id:</strong>
<span style="color:var(--special-uuid-color);">${item.id || ''}</span>`;
const line3 = document.createElement('div');
line3.className = 'special-data-item-line';
line3.innerHTML = `<strong style="color:var(--special-update-color);">update_time:</strong>
<span style="color:var(--special-update-color);">${item.update_time_shanghai || ''}</span>`;
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 = `<strong style="color:var(--special-title-color);">title:</strong>
<span style="color:var(--special-title-color);">${task.title || ''}</span>`;
line1.appendChild(leftSpan);
li.appendChild(line1);
const line2 = document.createElement('div');
line2.className = 'special-data-item-line';
line2.innerHTML = `<strong style="color:var(--special-uuid-color);">task_id:</strong>
<span style="color:var(--special-uuid-color);">${task.task_id || ''}</span>`;
const line3 = document.createElement('div');
line3.className = 'special-data-item-line';
line3.innerHTML = `<strong style="color:var(--special-update-color);">updated_at:</strong>
<span style="color:var(--special-update-color);">${task.updated_at_shanghai || ''}</span>`;
const line4 = document.createElement('div');
line4.className = 'special-data-item-line';
line4.innerHTML = `<strong style="color:var(--special-task-color);">original_conversation_id:</strong>
<span style="color:var(--special-task-color);">${task.original_conversation_id || ''}</span>`;
const line5 = document.createElement('div');
line5.className = 'special-data-item-line';
line5.innerHTML = `<strong style="color:var(--special-task-color);">conversation_id:</strong>
<span style="color:var(--special-task-color);">${task.conversation_id || ''}</span>`;
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);
})();