您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
一站式搜索、筛选、批量管理所有对话。强大的JSON导出(原始/自定义/含附件)。为聊天框注入新功能,如从任意消息分支、强制PDF深度解析等。
当前为
// ==UserScript== // @name ClaudePowerestManager&Enhancer // @name:zh-CN Claude神级拓展增强脚本 // @namespace http://tampermonkey.net/ // @version 1.1.3 // @description 一站式搜索、筛选、批量管理所有对话。强大的JSON导出(原始/自定义/含附件)。为聊天框注入新功能,如从任意消息分支、强制PDF深度解析等。 // @description:zh-CN [管理器] 右下角打开管理器面板开启一站式搜索、筛选、批量管理所有对话。强大的JSON导出(原始/自定义/含附件)。[增强器]为聊天框注入新功能,如从任意消息分支、强制PDF深度解析等。 // @description:en [Manager] Adds a button in the bottom-right corner to open a central panel for searching, filtering, and batch-managing all chats. Features a powerful exporter for raw/custom JSON with attachments. [Enhancer] Injects new buttons into the chat prompt toolbar for advanced real-time actions like branching from any message and forcing deep PDF analysis. // @author f14xuanlv // @license MIT // @homepageURL https://github.com/f14XuanLv/Claude-Powerest-Manager_Enhancer // @supportURL https://github.com/f14XuanLv/Claude-Powerest-Manager_Enhancer/issues // @match https://claude.ai/* // @include /^https:\/\/.*\.fuclaude\.[a-z]{3}\/.*$/ // @icon https://www.google.com/s2/favicons?sz=64&domain=claude.ai // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-start // ==/UserScript== (function(window) { 'use strict'; const LOG_PREFIX = "[ClaudePowerestManager&Enhancer v1.1.3]:"; console.log(LOG_PREFIX, "脚本已加载。"); function escapeHTML(str) { if (!str) return ''; return str.replace(/[&<>"']/g, function(match) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[match]; }); } // ========================================================================= // 0. 全局配置 // ========================================================================= const Config = { INITIAL_PARENT_UUID: "00000000-0000-4000-8000-000000000000", TOOLBAR_SELECTOR: 'div.relative.flex-1.flex.items-center.gap-2.shrink.min-w-0', EMPTY_AREA_SELECTOR: 'div.flex.flex-row.items-center.gap-2.min-w-0', FORCE_UPLOAD_TARGET_EXTENSIONS: [".pdf"], ATTACHMENT_PANEL_ID: 'cpm-attachment-preview-panel', EXPORT_MODAL_ID: 'cpm-export-modal', URL_GITHUB_REPO: 'https://github.com/f14XuanLv/Claude-Powerest-Manager_Enhancer', URL_STUDIO_REPO: 'https://github.com/f14XuanLv/claude-dialog-tree-studio' }; // ========================================================================= // 1. 设置模块注册表 // ========================================================================= /** * @typedef {object} ISettingModule - 设置模块接口定义 * @property {string} id - 模块的唯一ID。 * @property {string} title - 在设置面板中显示的标题。 * @property {function(): string} render - 返回该模块设置的HTML字符串。 * @property {function(HTMLElement): void} load - 从GM存储中加载设置并更新UI。 * @property {function(HTMLElement): void} save - 从UI读取设置并保存到GM存储。 * @property {function(HTMLElement): void} [addEventListeners] - (可选) 为模块的UI元素添加特定的事件监听器。 */ const SettingsRegistry = { /** @type {ISettingModule[]} */ modules: [], /** @param {ISettingModule} module */ register(module) { if (this.modules.find(m => m.id === module.id)) { console.warn(LOG_PREFIX, `尝试重复注册设置模块: ${module.id}`); return; } this.modules.push(module); console.log(LOG_PREFIX, `设置模块已注册: ${module.id}`); } }; // ========================================================================= // 2. 各功能模块定义 // ========================================================================= // --- 2.1 主题设置模块 --- const ThemeSettingsModule = { id: 'theme', title: '外观设置', render() { return ` <div class="cpm-setting-group"> <div class="cpm-setting-item"> <label for="cpm-theme-mode" class="cpm-settings-label">脚本主题:</label> <select id="cpm-theme-mode"> <option value="auto">跟随网站</option> <option value="light">锁定白天</option> <option value="dark">锁定黑夜</option> </select> </div> </div> `; }, load(container) { const themeSelect = container.querySelector('#cpm-theme-mode'); if (themeSelect) themeSelect.value = GM_getValue('themeMode', 'auto'); }, save(container) { const themeSelect = container.querySelector('#cpm-theme-mode'); if (themeSelect) { GM_setValue('themeMode', themeSelect.value); ThemeManager.applyCurrentTheme(); } } }; // --- 2.2 批量操作设置模块 --- const BatchOpsSettingsModule = { id: 'batchOps', title: '批量操作设置', render() { return ` <div class="cpm-setting-group"> <h4>批量收藏/取消收藏</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-refresh-after-star"><label for="cpm-refresh-after-star">操作后从服务器刷新列表 (否则仅更新当前视图)</label></div> </div> <div class="cpm-setting-group"> <h4>批量删除</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-refresh-after-delete"><label for="cpm-refresh-after-delete">操作后从服务器刷新列表 (否则仅更新当前视图)</label></div> </div> <div class="cpm-setting-group"> <h4>批量自动重命名</h4> <div class="cpm-setting-item"><label for="cpm-rename-lang" class="cpm-settings-label">标题语言:</label><input type="text" id="cpm-rename-lang" placeholder="例如:中文, English, 日本語"></div> <div class="cpm-setting-item"><label for="cpm-rename-rounds" class="cpm-settings-label">使用对话轮数 (最多):</label><input type="number" id="cpm-rename-rounds" min="1" max="10" step="1"></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-refresh-after-rename"><label for="cpm-refresh-after-rename">操作后从服务器刷新列表 (否则仅更新当前视图)</label></div> </div> `; }, load(container) { container.querySelector('#cpm-rename-lang').value = GM_getValue('renameLang', '中文'); container.querySelector('#cpm-rename-rounds').value = GM_getValue('renameRounds', '2'); container.querySelector('#cpm-refresh-after-rename').checked = GM_getValue('refreshAfterRename', false); container.querySelector('#cpm-refresh-after-star').checked = GM_getValue('refreshAfterStar', false); container.querySelector('#cpm-refresh-after-delete').checked = GM_getValue('refreshAfterDelete', false); }, save(container) { GM_setValue('renameLang', container.querySelector('#cpm-rename-lang').value); GM_setValue('renameRounds', container.querySelector('#cpm-rename-rounds').value); GM_setValue('refreshAfterRename', container.querySelector('#cpm-refresh-after-rename').checked); GM_setValue('refreshAfterStar', container.querySelector('#cpm-refresh-after-star').checked); GM_setValue('refreshAfterDelete', container.querySelector('#cpm-refresh-after-delete').checked); } }; // --- 2.3 导出设置模块 --- const ExportSettingsModule = { id: 'export', title: '自定义导出默认设置', render() { return ManagerUI.createExportSettingsHTML(true); }, load(container) { ManagerUI.loadExportSettings(container); }, save(container) { ManagerUI.saveExportSettings(container); }, addEventListeners(container) { ManagerUI.setupSubOptionDisabling(container); } }; // --- 2.4 注册所有设置模块 --- SettingsRegistry.register(ThemeSettingsModule); SettingsRegistry.register(BatchOpsSettingsModule); SettingsRegistry.register(ExportSettingsModule); // ========================================================================= // 3. 主题管理器 (共享) // ========================================================================= const ThemeManager = { init() { this.applyCurrentTheme(); const observer = new MutationObserver(() => this.applyCurrentTheme()); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-mode'] }); console.log(LOG_PREFIX, "主题管理器已初始化并开始监听。"); }, applyCurrentTheme() { const mode = GM_getValue('themeMode', 'auto'); let theme; if (mode === 'light' || mode === 'dark') { theme = mode; } else { theme = document.documentElement.getAttribute('data-mode') || 'light'; } document.body.setAttribute('cpm-theme', theme); }, }; // ========================================================================= // 4. API 层 (共享) // ========================================================================= const ClaudeAPI = { orgUuid: null, orgInfo: null, async getOrganizationInfo() { if (this.orgInfo) return this.orgInfo; try { const response = await fetch('/api/organizations'); if (!response.ok) throw new Error(`组织API请求失败: ${response.status}`); const orgs = await response.json(); if (orgs && orgs.length > 0) { this.orgInfo = orgs[0]; this.orgUuid = this.orgInfo.uuid; return this.orgInfo; } throw new Error("在API响应中未找到组织信息。"); } catch (error) { console.error(LOG_PREFIX, "获取组织信息失败:", error); throw error; } }, async getOrgUuid() { if (this.orgUuid) return this.orgUuid; const info = await this.getOrganizationInfo(); return info.uuid; }, async getConversations() { const orgId = await this.getOrgUuid(); const response = await fetch(`/api/organizations/${orgId}/chat_conversations`); if (!response.ok) throw new Error(`获取会话列表失败: ${response.status}`); return response.json(); }, async getConversationHistory(convUuid) { const orgId = await this.getOrgUuid(); const url = `/api/organizations/${orgId}/chat_conversations/${convUuid}?tree=True&rendering_mode=messages&render_all_tools=true`; const response = await fetch(url); if (!response.ok) throw new Error(`获取历史记录失败: ${response.status}`); return response.json(); }, async createTempConversation() { const orgId = await this.getOrgUuid(); const tempConvUuid = crypto.randomUUID(); await fetch(`/api/organizations/${orgId}/chat_conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: tempConvUuid, name: "" }) }); return tempConvUuid; }, async deleteConversations(convUuids) { const orgId = await this.getOrgUuid(); const isSingle = convUuids.length === 1; const url = isSingle ? `/api/organizations/${orgId}/chat_conversations/${convUuids[0]}` : `/api/organizations/${orgId}/chat_conversations/delete_many`; const options = { method: isSingle ? 'DELETE' : 'POST', headers: { 'Content-Type': 'application/json' } }; if (!isSingle) options.body = JSON.stringify({ conversation_uuids: convUuids }); const response = await fetch(url, options); if (!response.ok) throw new Error(`删除API请求失败: ${response.statusText}`); }, async generateTitle(tempConvUuid, messageContent) { const orgId = await this.getOrgUuid(); const url = `/api/organizations/${orgId}/chat_conversations/${tempConvUuid}/title`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message_content: messageContent, recent_titles: [] }) }); if (!response.ok) throw new Error("标题生成API请求失败。"); const { title } = await response.json(); if (!title || title.toLowerCase().includes('untitled')) throw new Error('生成了无效标题。'); return title; }, async updateConversation(convUuid, payload) { const orgId = await this.getOrgUuid(); const url = `/api/organizations/${orgId}/chat_conversations/${convUuid}`; const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error(`更新会话失败: ${response.statusText}`); }, async downloadFile(url) { const response = await fetch(url); if (!response.ok) throw new Error(`文件下载失败: ${response.status} at ${url}`); return response.blob(); } }; // ========================================================================= // 5. 共享UI与逻辑模块 // ========================================================================= const SharedLogic = { buildConversationTree(messages) { const nodes = {}; messages.forEach(msg => { nodes[msg.uuid] = msg; }); const childrenMap = {}; messages.forEach(msg => { const parentUuid = msg.parent_message_uuid || Config.INITIAL_PARENT_UUID; if (!childrenMap[parentUuid]) childrenMap[parentUuid] = []; childrenMap[parentUuid].push(msg.uuid); }); for (const parentUuid in childrenMap) { childrenMap[parentUuid].sort((a, b) => new Date(nodes[a].created_at) - new Date(nodes[b].created_at)); } function assignIdsRecursive(nodeUuid, prefix) { if (!nodes[nodeUuid]) return; nodes[nodeUuid].tree_id = prefix; const children = childrenMap[nodeUuid] || []; children.forEach((childUuid, index) => { assignIdsRecursive(childUuid, `${prefix}-${index}`); }); } const rootNodes = childrenMap[Config.INITIAL_PARENT_UUID] || []; rootNodes.forEach((rootUuid, index) => { assignIdsRecursive(rootUuid, `root-${index}`); }); return { nodes, childrenMap, rootNodes }; }, async renderTreeView(container, messages, options = {}) { const { isForBranching = false, onNodeClick = () => {} } = options; container.innerHTML = ''; if (!messages || messages.length === 0) { container.innerHTML = `<p class="cpm-loading">这是一个空对话${isForBranching ? ',无法选择分支点' : ''}。</p>`; return; } if (isForBranching) { const rootBtn = document.createElement('div'); rootBtn.id = 'cpm-branch-from-root-btn'; rootBtn.textContent = '从根节点开始 (创建一个新的主分支)'; rootBtn.onclick = () => onNodeClick(Config.INITIAL_PARENT_UUID, rootBtn); container.appendChild(rootBtn); } const { nodes, childrenMap, rootNodes } = this.buildConversationTree(messages); const orgUuid = await ClaudeAPI.getOrgUuid(); const baseUrl = window.location.origin; const renderNodeRecursive = (nodeUuid, indentLevel) => { const node = nodes[nodeUuid]; if (!node) return; const nodeElement = document.createElement('div'); nodeElement.className = 'cpm-tree-node'; nodeElement.style.paddingLeft = `${indentLevel * 20}px`; const sender = node.sender === 'human' ? 'You' : 'Claude'; const retryMarker = node.input_mode === 'retry' ? ' [Retry]' : ''; let textContent = Array.isArray(node.content) ? node.content.filter(b => b.type === 'text' && b.text).map(b => b.text.replace(/\n/g, ' ')).join(' ') : ''; if (!textContent && node.text) textContent = node.text.replace(/\n/g, ' '); const preview = textContent.substring(0, 80) + (textContent.length > 80 ? '...' : ''); let attachmentsHTML = ''; const allAttachments = []; const files_uuids = new Set(); if (node.attachments) { allAttachments.push(...node.attachments.map(file => ({ type: 'text', ...file }))); } if (node.files) { const binaryFiles = node.files.map(file => ({ type: 'binary', ...file })); allAttachments.push(...binaryFiles); binaryFiles.forEach(file => { if (file.file_uuid) files_uuids.add(file.file_uuid); }); } if (node.files_v2) { node.files_v2.forEach(file_v2 => { if (!file_v2.file_uuid || !files_uuids.has(file_v2.file_uuid)) { allAttachments.push({ type: 'binary', ...file_v2 }); } }); } if (allAttachments.length > 0) { attachmentsHTML += '<div class="cpm-tree-attachments">└─ [附件]:<ul>'; allAttachments.forEach(file => { if (file.type === 'text') { const contentPreview = (file.extracted_content || '').substring(0, 25); const escapedPreview = escapeHTML(contentPreview); attachmentsHTML += `<li>- ${file.file_name} <span class="cpm-attachment-source">[Source: convert_document]</span> <span class="cpm-attachment-details">[ID: ${file.id}] [Preview: "${escapedPreview}..."]</span></li>`; } else { // 增强URL构造逻辑以支持blob类型 let fullUrl = ''; if (file.document_asset?.url) { // 优先使用显式URL fullUrl = baseUrl + file.document_asset.url; } else if (file.preview_url) { // 其次使用预览URL fullUrl = baseUrl + file.preview_url; } else if (file.file_kind === 'blob' && orgUuid && file.file_uuid) { // **新增**: 处理 blob 类型 fullUrl = `${baseUrl}/api/organizations/${orgUuid}/files/${file.file_uuid}/contents`; } else if (orgUuid && file.file_uuid && file.file_name) { // 回退到旧的文档格式 const ext = file.file_name.includes('.') ? file.file_name.rsplit('.', 1)[1] : ''; if (ext) fullUrl = `${baseUrl}/api/${orgUuid}/files/${file.file_uuid}/document_${ext.replace('.','')}/${file.file_name}`; } const urlLink = fullUrl ? `<a href="${fullUrl}" target="_blank" class="cpm-attachment-url" title="点击在新标签页打开: ${fullUrl}">[View/Download URL]</a>` : '[URL Not Available]'; attachmentsHTML += `<li>- ${file.file_name} <span class="cpm-attachment-source">[Source: /upload | Type: ${file.file_kind || 'unknown'}]</span> ${urlLink}</li>`; } }); attachmentsHTML += '</ul></div>'; } nodeElement.innerHTML = ` <div class="cpm-tree-node-header"> <span class="cpm-tree-node-id">[${node.tree_id}]</span> <span class="cpm-tree-node-sender sender-${sender.toLowerCase()}">${sender}${retryMarker}:</span> <span class="cpm-tree-node-preview">${preview || '[仅包含附件或工具使用]'}</span> </div> ${attachmentsHTML}`; if (isForBranching && node.sender === 'assistant') { nodeElement.classList.add('cpm-branch-node-clickable'); nodeElement.title = `点击从此节点继续对话`; nodeElement.onclick = () => onNodeClick(node.uuid, nodeElement); } container.appendChild(nodeElement); (childrenMap[nodeUuid] || []).forEach(childUuid => renderNodeRecursive(childUuid, indentLevel + 1)); }; rootNodes.forEach(rootUuid => renderNodeRecursive(rootUuid, 0)); } }; // ========================================================================= // 6. 业务逻辑层 (Service Layer) // ========================================================================= const ManagerService = { conversationsCache: [], async loadConversations() { this.conversationsCache = await ClaudeAPI.getConversations(); return this.conversationsCache; }, async performManualRename(convUuid, newTitle) { await ClaudeAPI.updateConversation(convUuid, { name: newTitle }); const cachedItem = this.conversationsCache.find(c => c.uuid === convUuid); if (cachedItem) cachedItem.name = newTitle; return true; }, async exportAttachmentsForConversation(historyData, exportDirHandle, statusCallback) { const { nodes } = SharedLogic.buildConversationTree(historyData.chat_messages); const allAttachments = []; for (const node of Object.values(nodes)) { (node.attachments || []).forEach(file => allAttachments.push({ type: 'text', content: file.extracted_content, node_id: node.tree_id, ...file })); (node.files || []).forEach(file => allAttachments.push({ type: 'binary', node_id: node.tree_id, ...file })); (node.files_v2 || []).forEach(file => allAttachments.push({ type: 'binary', node_id: node.tree_id, ...file })); } if (allAttachments.length > 0) { statusCallback(`发现 ${allAttachments.length} 个附件,开始下载...`, 'info'); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error("无法获取组织信息以下载附件。"); for (let i = 0; i < allAttachments.length; i++) { const file = allAttachments[i]; let fileName; const baseName = file.file_name ? (file.file_name.includes('.') ? file.file_name.substring(0, file.file_name.lastIndexOf('.')) : file.file_name) : 'unknown_file'; const extension = file.file_name ? (file.file_name.includes('.') ? file.file_name.substring(file.file_name.lastIndexOf('.')) : '') : ''; if (file.type === 'text') { fileName = `${baseName}_[${file.id || 'no-id'}]_[${file.node_id || 'no-node'}].txt`; } else if (file.type === 'binary' && file.file_uuid) { fileName = `${baseName}_[${file.file_uuid}]${extension}`; } if (!fileName) continue; try { await exportDirHandle.getFileHandle(fileName, { create: false }); statusCallback(`(${i + 1}/${allAttachments.length}) 跳过 (文件已存在): ${fileName}`, 'info'); continue; } catch (error) { if (error.name !== 'NotFoundError') { console.error(`检查文件 ${fileName} 时发生意外错误:`, error); statusCallback(`检查文件 ${fileName} 出错`, 'error'); continue; } } statusCallback(`(${i + 1}/${allAttachments.length}) 正在下载: ${fileName}`, 'info'); try { let fileContent; if (file.type === 'text') { fileContent = new Blob([file.content || ""], { type: 'text/plain;charset=utf-8' }); } else { // 增强URL构造逻辑以支持blob类型 let downloadUrl; if (file.document_asset?.url) { // 优先使用显式URL downloadUrl = file.document_asset.url; } else if (file.preview_url) { // 其次使用预览URL downloadUrl = file.preview_url; } else if (file.file_kind === 'blob' && orgInfo.uuid && file.file_uuid) { // **新增**: 处理 blob 类型 downloadUrl = `/api/organizations/${orgInfo.uuid}/files/${file.file_uuid}/contents`; } else if (orgInfo.uuid && file.file_uuid && file.file_name) { // 回退到旧的文档格式 const ext = file.file_name.includes('.') ? file.file_name.rsplit('.', 1)[1] : ''; downloadUrl = `/api/${orgInfo.uuid}/files/${file.file_uuid}/document_${ext.replace('.','')}/${file.file_name}`; } if(!downloadUrl) throw new Error("找不到附件的下载链接。"); fileContent = await ClaudeAPI.downloadFile(downloadUrl); } const fileHandle = await exportDirHandle.getFileHandle(fileName, { create: true }); const writable = await fileHandle.createWritable(); await writable.write(fileContent); await writable.close(); } catch (err) { console.error(`处理附件 ${fileName} 失败:`, err); statusCallback(`处理附件 ${fileName} 失败`, 'error'); } } } }, async performExportOriginal(convUuid, statusCallback) { if (typeof window.showDirectoryPicker !== 'function') throw new Error("您的浏览器不支持 File System Access API。"); statusCallback("正在请求文件夹权限...", 'info'); let rootDirHandle; try { rootDirHandle = await window.showDirectoryPicker(); } catch (err) { if (err.name === 'AbortError') { statusCallback("用户取消了文件夹选择。", 'info', 3000); return; } throw err; } try { const historyData = await ClaudeAPI.getConversationHistory(convUuid); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error("缺少导出所需组织信息。"); statusCallback("正在创建目录...", 'info'); const orgName = (orgInfo.name || "unknown_org").replace(/'s Organization$/, ""); const safeTitle = (historyData.name || "Untitled").replace(/[<>:"/\\|?*]/g, '_'); const pathParts = [`Claude_Exports`, `[${orgName}]`, `[Original]_[${safeTitle}]_[${convUuid}]`]; let currentDirHandle = rootDirHandle; for (const part of pathParts) { currentDirHandle = await currentDirHandle.getDirectoryHandle(part, { create: true }); } const exportDirHandle = currentDirHandle; const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); const historyFileName = `history-${timestamp}.json`; statusCallback(`正在写入 ${historyFileName}...`, 'info'); const historyFileHandle = await exportDirHandle.getFileHandle(historyFileName, { create: true }); const writableHistory = await historyFileHandle.createWritable(); await writableHistory.write(JSON.stringify(historyData, null, 2)); await writableHistory.close(); await this.exportAttachmentsForConversation(historyData, exportDirHandle, statusCallback); statusCallback("原始导出完成!", 'success', 5000); } catch (error) { console.error("原始导出失败:", error); statusCallback(`原始导出失败: ${error.message}`, 'error', 5000); } }, transformConversation(originalData, settings) { const newData = {}; if (settings.metadata.include) { if (settings.metadata.title) newData.name = originalData.name; if (settings.metadata.summary) newData.summary = originalData.summary; if (settings.metadata.main_timestamps) { newData.created_at = originalData.created_at; newData.updated_at = originalData.updated_at; } if (settings.metadata.conv_settings) newData.settings = originalData.settings; } newData.chat_messages = originalData.chat_messages.map(originalMsg => { const newMsg = { }; if (settings.message.sender) newMsg.sender = originalMsg.sender; if (settings.message.uuids) { newMsg.uuid = originalMsg.uuid; newMsg.parent_message_uuid = originalMsg.parent_message_uuid; } if (settings.message.timestamps.messageNode) { newMsg.created_at = originalMsg.created_at; newMsg.updated_at = originalMsg.updated_at; } if (settings.message.other_meta) { newMsg.index = originalMsg.index; newMsg.stop_reason = originalMsg.stop_reason; newMsg.truncated = originalMsg.truncated; } if (originalMsg.text) newMsg.text = originalMsg.text; if (originalMsg.content && Array.isArray(originalMsg.content)) { newMsg.content = originalMsg.content.map(block => { const newBlock = {...block}; if (!settings.message.timestamps.contentBlock) { delete newBlock.start_timestamp; delete newBlock.stop_timestamp; } return newBlock; }).filter(block => { switch (block.type) { case 'text': return settings.content.text; case 'thinking': return settings.advanced.thinking; case 'tool_use': case 'tool_result': if (!settings.advanced.tools.include) return false; if (settings.advanced.tools.onlySuccessful && block.is_error) return false; switch (block.name) { case 'web_search': return settings.advanced.tools.web_search; case 'repl': return settings.advanced.tools.repl; case 'artifacts': return settings.advanced.tools.artifacts; default: return settings.advanced.tools.other; } default: return true; } }); } const processAttachments = (attachments) => { if (!attachments) return undefined; if (settings.attachments.mode === 'none') return undefined; if (settings.attachments.mode === 'full') { if (settings.message.timestamps.attachment) return attachments; return attachments.map(att => { const newAtt = {...att}; delete newAtt.created_at; return newAtt; }); } if (settings.attachments.mode === 'metadata_only') { return attachments.map(att => ({ id: att.id, file_uuid: att.file_uuid, file_name: att.file_name, file_size: att.file_size, file_type: att.file_type, file_kind: att.file_kind })); } }; const attachmentsResult = processAttachments(originalMsg.attachments); const filesResult = processAttachments(originalMsg.files); const filesV2Result = processAttachments(originalMsg.files_v2); if (attachmentsResult) newMsg.attachments = attachmentsResult; if (filesResult) newMsg.files = filesResult; if (filesV2Result) newMsg.files_v2 = filesV2Result; return newMsg; }); return newData; }, async performExportCustom(convUuid, settings, statusCallback) { if (typeof window.showDirectoryPicker !== 'function') throw new Error("您的浏览器不支持 File System Access API。"); statusCallback("正在请求文件夹权限...", 'info'); let rootDirHandle; try { rootDirHandle = await window.showDirectoryPicker(); } catch (err) { if (err.name === 'AbortError') { statusCallback("用户取消了文件夹选择。", 'info', 3000); return; } throw err; } try { const historyData = await ClaudeAPI.getConversationHistory(convUuid); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error("缺少导出所需组织信息。"); statusCallback("正在创建目录...", 'info'); const orgName = (orgInfo.name || "unknown_org").replace(/'s Organization$/, ""); const safeTitle = (historyData.name || "Untitled").replace(/[<>:"/\\|?*]/g, '_'); const pathParts = [`Claude_Exports`, `[${orgName}]`, `[Custom]_[${safeTitle}]_[${convUuid}]`]; let currentDirHandle = rootDirHandle; for (const part of pathParts) { currentDirHandle = await currentDirHandle.getDirectoryHandle(part, { create: true }); } const exportDirHandle = currentDirHandle; statusCallback("正在根据设置转换数据...", 'info'); const transformedData = this.transformConversation(historyData, settings); const jsonString = JSON.stringify(transformedData, null, 2); const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); const historyFileName = `history-${timestamp}.json`; statusCallback(`正在写入 ${historyFileName}...`, 'info'); const historyFileHandle = await exportDirHandle.getFileHandle(historyFileName, { create: true }); const writableHistory = await historyFileHandle.createWritable(); await writableHistory.write(jsonString); await writableHistory.close(); if (settings.attachments.mode !== 'none') { await this.exportAttachmentsForConversation(historyData, exportDirHandle, statusCallback); } statusCallback("自定义导出完成!", 'success', 5000); } catch (error) { console.error("自定义导出失败:", error); statusCallback(`自定义导出失败: ${error.message}`, 'error', 5000); } }, async performAutoRename(convUuid) { const langPrompt = GM_getValue('renameLang', '中文'); const maxRounds = parseInt(GM_getValue('renameRounds', 2), 10); const historyData = await ClaudeAPI.getConversationHistory(convUuid); const roundsToUse = Math.min(Math.floor(historyData.chat_messages.length / 2), maxRounds); if (roundsToUse < 1) throw new Error("对话轮次不足(可能为空对话),跳过重命名。"); const messagesToProcess = historyData.chat_messages.slice(0, roundsToUse * 2); let messageParts = []; messagesToProcess.forEach((msg, index) => { const senderLabel = `Message ${index + 1} (${msg.sender === 'human' ? 'User' : 'Assistant'})`; let textContent = Array.isArray(msg.content) ? msg.content.filter(b => b.type === 'text' && b.text).map(b => b.text).join('\n') : ''; if (!textContent && msg.text) textContent = msg.text; if (textContent.trim()) messageParts.push(`${senderLabel}:\n\n${textContent.trim()}`); }); if (messageParts.length === 0) throw new Error("在指定轮次内未找到有效文本内容。"); let finalMessageContent = messageParts.join('\n\n'); if (langPrompt && langPrompt.trim() !== "") { const startInstruction = `TASK: Generate a title for the following conversation.\nRULE: The title language must be strictly ${langPrompt}.\n\n--- Conversation Start ---`; const endInstruction = `\n--- Conversation End ---\nREMINDER: Generate the title in ${langPrompt} now.`; finalMessageContent = `${startInstruction}\n\n${finalMessageContent}\n${endInstruction}`; } const tempConvUuid = await ClaudeAPI.createTempConversation(); try { const newTitle = await ClaudeAPI.generateTitle(tempConvUuid, finalMessageContent); await ClaudeAPI.updateConversation(convUuid, { name: newTitle }); const cachedItem = this.conversationsCache.find(c => c.uuid === convUuid); if (cachedItem) cachedItem.name = newTitle; return newTitle; } finally { await ClaudeAPI.deleteConversations([tempConvUuid]); } }, async performBatchStarAction(uuids, isStarring) { let successCount = 0; for (const uuid of uuids) { try { await ClaudeAPI.updateConversation(uuid, { is_starred: isStarring }); const cachedItem = this.conversationsCache.find(c => c.uuid === uuid); if (cachedItem) cachedItem.is_starred = isStarring; successCount++; } catch (error) { console.error(`(取消)收藏 ${uuid} 失败:`, error); } await new Promise(resolve => setTimeout(resolve, 300)); } return successCount; }, async performBatchDelete(uuids) { await ClaudeAPI.deleteConversations(uuids); this.conversationsCache = this.conversationsCache.filter(c => !uuids.includes(c.uuid)); return uuids.length; } }; // ========================================================================= // 7. 主管理器UI层 (ManagerUI) // ========================================================================= const ManagerUI = { currentSort: 'updated_at_desc', currentFilter: 'all', currentSearch: '', statusTimeout: null, isInitialized: false, init() { if (this.isInitialized) return; this.createUI(); this.bindEvents(); ClaudeAPI.getOrgUuid().catch(err => console.error(LOG_PREFIX, "预获取OrgId失败", err)); this.isInitialized = true; console.log(LOG_PREFIX, "主管理器UI已初始化。"); }, createUI() { const svgDefs = document.createElement('div'); svgDefs.style.display = 'none'; svgDefs.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg"> <defs> <symbol id="cpm-icon-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></symbol> <symbol id="cpm-icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></symbol> <symbol id="cpm-icon-edit" viewBox="0 0 24 24"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></symbol> <symbol id="cpm-icon-tree" viewBox="0 0 24 24"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></symbol> <symbol id="cpm-icon-export-original" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></symbol> <symbol id="cpm-icon-export-custom" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /><g transform="translate(16, 3) scale(0.5)" fill="currentColor" stroke="none"><path fill-rule="evenodd" d="M6.455 1.45A.5.5 0 0 1 6.952 1h2.096a.5.5 0 0 1 .497.45l.186 1.858a4.996 4.996 0 0 1 1.466.848l1.703-.769a.5.5 0 0 1 .639.206l1.047 1.814a.5.5 0 0 1-.14.656l-1.517 1.09a5.026 5.026 0 0 1 0 1.694l1.516 1.09a.5.5 0 0 1 .141.656l-1.047 1.814a.5.5 0 0 1-.639.206l-1.703-.768c-.433.36-.928.649-1.466.847l-.186 1.858a.5.5 0 0 1-.497.45H6.952a.5.5 0 0 1-.497-.45l-.186-1.858a4.993 4.993 0 0 1-1.466-.848l-1.703.769a.5.5 0 0 1-.639-.206l-1.047-1.814a.5.5 0 0 1 .14-.656l1.517-1.09a5.033 5.033 0 0 1 0-1.694l-1.516-1.09a.5.5 0 0 1-.141-.656L2.46 3.593a.5.5 0 0 1 .639-.206l1.703.769c.433-.36.928.65 1.466-.848l.186-1.858Zm-.177 7.567-.022-.037a2 2 0 0 1 3.466-1.997l.022.037a2 2 0 0 1-3.466 1.997Z" clip-rule="evenodd" /></g></symbol> <symbol id="cpm-icon-save" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"></path></symbol> <symbol id="cpm-icon-cancel" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol> <symbol id="cpm-icon-github" viewBox="0 0 24 24"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></symbol> <symbol id="cpm-icon-studio" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="6" y1="3" x2="6" y2="15" stroke-linecap="round"></line><circle cx="16" cy="8" r="2"></circle><circle cx="6" cy="18" r="2"></circle><path d="M16 11a8 8 0 0 1 -8 7" stroke-linecap="round"></path><g transform="translate(14.5, 14.5) scale(0.5)" fill="currentColor" stroke="none"><path fill-rule="evenodd" d="M6.455 1.45A.5.5 0 0 1 6.952 1h2.096a.5.5 0 0 1 .497.45l.186 1.858a4.996 4.996 0 0 1 1.466.848l1.703-.769a.5.5 0 0 1 .639.206l1.047 1.814a.5.5 0 0 1-.14.656l-1.517 1.09a5.026 5.026 0 0 1 0 1.694l1.516 1.09a.5.5 0 0 1 .141.656l-1.047 1.814a.5.5 0 0 1-.639.206l-1.703-.768c-.433.36-.928.649-1.466.847l-.186 1.858a.5.5 0 0 1-.497.45H6.952a.5.5 0 0 1-.497-.45l-.186-1.858a4.993 4.993 0 0 1-1.466-.848l-1.703.769a.5.5 0 0 1-.639-.206l-1.047-1.814a.5.5 0 0 1 .14-.656l1.517-1.09a5.033 5.033 0 0 1 0-1.694l-1.516-1.09a.5.5 0 0 1-.141-.656L2.46 3.593a.5.5 0 0 1 .639-.206l1.703.769c.433-.36.928.65 1.466-.848l.186-1.858Zm-.177 7.567-.022-.037a2 2 0 0 1 3.466-1.997l.022.037a2 2 0 0 1-3.466 1.997Z" clip-rule="evenodd" /></g></symbol> </defs> </svg> `; document.body.appendChild(svgDefs); const managerButton = document.createElement('button'); managerButton.id = 'cpm-manager-button'; managerButton.innerHTML = 'Manager'; document.body.appendChild(managerButton); const mainPanel = document.createElement('div'); mainPanel.id = 'cpm-main-panel'; mainPanel.className = 'cpm-panel'; mainPanel.innerHTML = ` <div class="cpm-header"> <h2>Manager</h2> <div class="cpm-header-actions"> <a href="${Config.URL_GITHUB_REPO}" target="_blank" class="cpm-icon-btn" title="查看 GitHub 仓库"><svg class="cpm-svg-icon" stroke-width="1.5"><use href="#cpm-icon-github"></use></svg></a> <a href="${Config.URL_STUDIO_REPO}" target="_blank" class="cpm-icon-btn" title="了解下一个项目: claude-dialog-tree-studio"><svg class="cpm-svg-icon" stroke-width="1.5"><use href="#cpm-icon-studio"></use></svg></a> <button id="cpm-open-settings-button" title="设置" class="cpm-icon-btn"><svg class="cpm-svg-icon"><use href="#cpm-icon-settings"></use></svg></button> <button class="cpm-close-button cpm-icon-btn">×</button> </div> </div> <div class="cpm-toolbar"> <div class="cpm-toolbar-group"><button class="cpm-btn" id="cpm-select-all">全选</button><button class="cpm-btn" id="cpm-select-none">全不选</button><button class="cpm-btn" id="cpm-select-invert">反选</button></div> <div class="cpm-toolbar-group"><input type="search" id="cpm-search-box" placeholder="搜索标题..."/></div> <div class="cpm-toolbar-group"><label>排序:</label><select id="cpm-sort-select"><option value="updated_at_desc">时间降序</option><option value="updated_at_asc">时间升序</option><option value="name_asc">名称 A-Z</option><option value="name_desc">名称 Z-A</option></select></div> <div class="cpm-toolbar-group"><label>筛选:</label><select id="cpm-filter-select"><option value="all">显示全部</option><option value="starred">仅显示收藏</option><option value="unstarred">隐藏收藏</option><option value="ascii_only">仅显示纯ASCII标题</option><option value="non_ascii">不显示纯ASCII标题</option></select></div> <button class="cpm-icon-btn" id="cpm-refresh" title="刷新列表"><svg class="cpm-svg-icon"><use href="#cpm-icon-refresh"></use></svg></button> </div> <div class="cpm-actions"><button class="cpm-action-btn" id="cpm-batch-star">批量收藏</button><button class="cpm-action-btn" id="cpm-batch-unstar">批量取消收藏</button><button class="cpm-action-btn" id="cpm-batch-rename">批量自动重命名</button><button class="cpm-action-btn cpm-danger-btn" id="cpm-batch-delete">批量删除</button></div> <div class="cpm-list-container"><p class="cpm-loading">点击刷新按钮 ( <svg class="cpm-svg-icon"><use href="#cpm-icon-refresh"></use></svg> ) 加载会话列表。</p></div> <div class="cpm-status-bar">准备就绪。</div>`; document.body.appendChild(mainPanel); const settingsPanel = document.createElement('div'); settingsPanel.id = 'cpm-settings-panel'; settingsPanel.className = 'cpm-panel'; const settingsHeader = `<div class="cpm-header"><h2>管理器设置</h2><button class="cpm-close-button cpm-icon-btn">×</button></div>`; const settingsContent = document.createElement('div'); settingsContent.className = 'cpm-settings-content'; for (const module of SettingsRegistry.modules) { const section = document.createElement('div'); section.className = 'cpm-setting-section'; section.innerHTML = `<h3 class="cpm-setting-section-title">${module.title}</h3>` + module.render(); settingsContent.appendChild(section); } const settingsButtons = `<div class="cpm-settings-buttons"><button id="cpm-back-to-main" class="cpm-btn">返回主面板</button><button id="cpm-save-settings-button" class="cpm-btn cpm-primary-btn">保存设置</button></div>`; settingsPanel.innerHTML = settingsHeader; settingsPanel.appendChild(settingsContent); settingsPanel.insertAdjacentHTML('beforeend', settingsButtons); document.body.appendChild(settingsPanel); const treePanel = document.createElement('div'); treePanel.id = 'cpm-tree-panel'; treePanel.className = 'cpm-panel cpm-tree-panel-override'; treePanel.innerHTML = ` <div class="cpm-header"><h2 id="cpm-tree-title">对话树预览</h2><button id="cpm-tree-close-button" class="cpm-icon-btn">×</button></div> <div id="cpm-tree-container" class="cpm-tree-container"><p class="cpm-loading">正在加载对话树...</p></div>`; document.body.appendChild(treePanel); }, bindEvents() { document.getElementById('cpm-manager-button').onclick = () => this.togglePanel('cpm-main-panel'); document.querySelectorAll('.cpm-close-button').forEach(btn => btn.onclick = () => this.hideAllPanels()); document.getElementById('cpm-open-settings-button').onclick = () => this.togglePanel('cpm-settings-panel'); document.getElementById('cpm-back-to-main').onclick = () => this.togglePanel('cpm-main-panel'); document.getElementById('cpm-refresh').onclick = () => this.loadConversations(); document.getElementById('cpm-select-all').onclick = () => this.selectAll(true); document.getElementById('cpm-select-none').onclick = () => this.selectAll(false); document.getElementById('cpm-select-invert').onclick = () => this.selectInvert(); document.getElementById('cpm-search-box').oninput = (e) => { this.currentSearch = e.target.value; this.renderConversationList(); }; document.getElementById('cpm-sort-select').onchange = (e) => { this.currentSort = e.target.value; this.renderConversationList(); }; document.getElementById('cpm-filter-select').onchange = (e) => { this.currentFilter = e.target.value; this.renderConversationList(); }; document.getElementById('cpm-batch-rename').onclick = () => this.handleBatchRename(); document.getElementById('cpm-batch-delete').onclick = () => this.handleBatchDelete(); document.getElementById('cpm-batch-star').onclick = () => this.handleBatchStar(true); document.getElementById('cpm-batch-unstar').onclick = () => this.handleBatchStar(false); document.getElementById('cpm-save-settings-button').onclick = () => this.saveSettings(); document.getElementById('cpm-tree-close-button').onclick = () => this.hidePanel('cpm-tree-panel'); document.querySelector('#cpm-main-panel .cpm-list-container').addEventListener('click', (e) => { const li = e.target.closest('li'); if (!li) return; const uuid = li.dataset.uuid; if (e.target.closest('.cpm-action-rename')) this.enterEditMode(li); else if (e.target.closest('.cpm-action-tree')) this.handleTreeView(uuid); else if (e.target.closest('.cpm-action-export-original')) this.handleExport(uuid, 'original'); else if (e.target.closest('.cpm-action-export-custom')) this.handleExport(uuid, 'custom'); else if (e.target.closest('.cpm-action-save')) this.handleSaveRename(li); else if (e.target.closest('.cpm-action-cancel')) this.exitEditMode(li); }); }, togglePanel(panelId) { const panel = document.getElementById(panelId); const isVisible = panel.style.display === 'flex'; this.hideAllPanels(); if (!isVisible) { panel.style.display = 'flex'; if (panelId === 'cpm-main-panel' && ManagerService.conversationsCache.length === 0) this.loadConversations(); if (panelId === 'cpm-settings-panel') this.loadSettings(); } }, hidePanel(panelId) { document.getElementById(panelId).style.display = 'none'; }, hideAllPanels() { document.querySelectorAll('.cpm-panel').forEach(p => p.style.display = 'none'); document.querySelector('.cpm-modal-overlay')?.remove(); }, loadSettings() { const panel = document.getElementById('cpm-settings-panel'); if (!panel) return; for (const module of SettingsRegistry.modules) { module.load(panel); module.addEventListeners?.(panel); } }, saveSettings() { const panel = document.getElementById('cpm-settings-panel'); if (!panel) return; for (const module of SettingsRegistry.modules) { module.save(panel); } this.updateStatus('设置已保存!', 'success', 3000); this.togglePanel('cpm-main-panel'); }, async loadConversations() { const listContainer = document.querySelector('#cpm-main-panel .cpm-list-container'); listContainer.innerHTML = '<p class="cpm-loading">正在加载会话列表...</p>'; this.updateStatus("正在获取会话列表...", 'info'); try { const convos = await ManagerService.loadConversations(); this.renderConversationList(); this.updateStatus(`已加载 ${convos.length} 个会话。`, 'info'); } catch (error) { listContainer.innerHTML = `<p class="cpm-error">加载会话失败: ${error.message}</p>`; this.updateStatus("加载失败。", 'error'); } }, escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, renderConversationList() { const listContainer = document.querySelector('#cpm-main-panel .cpm-list-container'); let conversationsToRender = [...ManagerService.conversationsCache]; if (this.currentSearch) { const searchPattern = new RegExp(this.escapeRegExp(this.currentSearch), 'i'); conversationsToRender = conversationsToRender.filter(c => searchPattern.test(c.name || '')); } if (this.currentFilter === 'starred') conversationsToRender = conversationsToRender.filter(c => c.is_starred); else if (this.currentFilter === 'unstarred') conversationsToRender = conversationsToRender.filter(c => !c.is_starred); else if (this.currentFilter === 'ascii_only') conversationsToRender = conversationsToRender.filter(c => /^[\x00-\x7F]*$/.test(c.name || '')); else if (this.currentFilter === 'non_ascii') conversationsToRender = conversationsToRender.filter(c => /[^\x00-\x7F]/.test(c.name || '')); conversationsToRender.sort((a, b) => { switch (this.currentSort) { case 'updated_at_asc': return new Date(a.updated_at) - new Date(b.updated_at); case 'name_asc': return (a.name || '').localeCompare(b.name || ''); case 'name_desc': return (b.name || '').localeCompare(a.name || ''); default: return new Date(b.updated_at) - new Date(a.updated_at); } }); if (conversationsToRender.length === 0) { listContainer.innerHTML = '<p>没有符合条件的会话。</p>'; return; } const ul = document.createElement('ul'); ul.className = 'cpm-convo-list'; conversationsToRender.forEach(convo => { const li = document.createElement('li'); li.dataset.uuid = convo.uuid; const titleText = convo.name || '无标题对话'; let highlightedTitle = titleText; if (this.currentSearch) highlightedTitle = titleText.replace(new RegExp(this.escapeRegExp(this.currentSearch), 'gi'), (match) => `<span class="cpm-highlight">${match}</span>`); const star = convo.is_starred ? '<span class="cpm-star">★</span>' : ''; li.innerHTML = ` <input type="checkbox" class="cpm-checkbox" data-uuid="${convo.uuid}"> <div class="cpm-convo-details"><span class="cpm-convo-title">${star}${highlightedTitle}</span><span class="cpm-convo-date">${new Date(convo.updated_at).toLocaleString()}</span></div> <div class="cpm-convo-actions"> <button class="cpm-icon-btn cpm-action-rename" title="手动重命名"><svg class="cpm-svg-icon"><use href="#cpm-icon-edit"></use></svg></button> <button class="cpm-icon-btn cpm-action-tree" title="预览对话树"><svg class="cpm-svg-icon"><use href="#cpm-icon-tree"></use></svg></button> <button class="cpm-icon-btn cpm-action-export-original" title="原始JSON导出"><svg class="cpm-svg-icon" stroke-width="1.5"><use href="#cpm-icon-export-original"></use></svg></button> <button class="cpm-icon-btn cpm-action-export-custom" title="自定义JSON导出"><svg class="cpm-svg-icon" stroke-width="1.5"><use href="#cpm-icon-export-custom"></use></svg></button> </div>`; ul.appendChild(li); }); listContainer.innerHTML = ''; listContainer.appendChild(ul); }, enterEditMode(li) { const currentlyEditing = document.querySelector('li.is-editing'); if (currentlyEditing && currentlyEditing !== li) this.exitEditMode(currentlyEditing); li.classList.add('is-editing'); const detailsDiv = li.querySelector('.cpm-convo-details'); const actionsDiv = li.querySelector('.cpm-convo-actions'); const titleSpan = li.querySelector('.cpm-convo-title'); const originalTitle = titleSpan.textContent.replace(/★/g, '').trim(); li.dataset.originalDetails = detailsDiv.innerHTML; li.dataset.originalActions = actionsDiv.innerHTML; detailsDiv.innerHTML = `<input type="text" class="cpm-edit-input" value="${originalTitle}">`; actionsDiv.innerHTML = `<button class="cpm-icon-btn cpm-action-save" title="保存"><svg class="cpm-svg-icon"><use href="#cpm-icon-save"></use></svg></button><button class="cpm-icon-btn cpm-action-cancel" title="取消"><svg class="cpm-svg-icon"><use href="#cpm-icon-cancel"></use></svg></button>`; const input = detailsDiv.querySelector('.cpm-edit-input'); input.focus(); input.select(); input.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); this.handleSaveRename(li); } else if (e.key === 'Escape') { this.exitEditMode(li); } }; }, exitEditMode(li) { if (!li.classList.contains('is-editing')) return; li.classList.remove('is-editing'); li.querySelector('.cpm-convo-details').innerHTML = li.dataset.originalDetails; li.querySelector('.cpm-convo-actions').innerHTML = li.dataset.originalActions; delete li.dataset.originalDetails; delete li.dataset.originalActions; }, async handleSaveRename(li) { const uuid = li.dataset.uuid; const input = li.querySelector('.cpm-edit-input'); const newTitle = input.value.trim(); const originalTitle = li.dataset.originalDetails.match(/<span class="cpm-convo-title">(.*?)<\/span>/)[1].replace(/<[^>]*>/g, '').replace(/★/g, '').trim(); if (!newTitle || newTitle === originalTitle) { this.exitEditMode(li); return; } input.disabled = true; this.updateStatus(`正在保存新标题...`, 'info'); try { await ManagerService.performManualRename(uuid, newTitle); this.updateStatus("保存成功!", 'success'); const convo = ManagerService.conversationsCache.find(c => c.uuid === uuid); const star = convo.is_starred ? '<span class="cpm-star">★</span>' : ''; li.dataset.originalDetails = li.dataset.originalDetails.replace(/>(★)?.*?<\/span>/, `>${star}${newTitle}</span>`); this.exitEditMode(li); } catch (error) { this.updateStatus(`保存失败: ${error.message}`, 'error'); input.disabled = false; input.focus(); } }, async handleTreeView(uuid) { const treePanel = document.getElementById('cpm-tree-panel'); const treeContainer = document.getElementById('cpm-tree-container'); const treeTitle = document.getElementById('cpm-tree-title'); const convo = ManagerService.conversationsCache.find(c => c.uuid === uuid); treeTitle.textContent = `对话树: ${convo ? (convo.name || '无标题') : '加载中...'}`; treeContainer.innerHTML = '<p class="cpm-loading">正在加载对话历史...</p>'; treePanel.style.display = 'flex'; try { const historyData = await ClaudeAPI.getConversationHistory(uuid); await SharedLogic.renderTreeView(treeContainer, historyData.chat_messages); } catch (error) { console.error(error); treeContainer.innerHTML = `<p class="cpm-error">无法加载对话树: ${error.message}</p>`; } }, async handleExport(uuid, type) { if (type === 'original') { await ManagerService.performExportOriginal(uuid, this.updateStatus.bind(this)); } else if (type === 'custom') { this.showExportModal(uuid); } }, selectAll(checked) { document.querySelectorAll('.cpm-list-container .cpm-checkbox').forEach(cb => cb.checked = checked); }, selectInvert() { document.querySelectorAll('.cpm-list-container .cpm-checkbox').forEach(cb => cb.checked = !cb.checked); }, getSelectedUuids() { return Array.from(document.querySelectorAll('.cpm-checkbox:checked')).map(cb => cb.dataset.uuid); }, updateStatus(message, type = 'info', timeout = 0) { if (this.statusTimeout) clearTimeout(this.statusTimeout); const s = document.querySelector('#cpm-main-panel .cpm-status-bar'); s.textContent = message; s.classList.remove('is-error', 'is-success'); if (type === 'error') s.classList.add('is-error'); else if (type === 'success') s.classList.add('is-success'); if (timeout > 0) { this.statusTimeout = setTimeout(() => { s.textContent = '准备就绪。'; s.classList.remove('is-error', 'is-success'); }, timeout); } }, async handleBatchOperation(opName, serviceFunc, ...args) { const uuids = this.getSelectedUuids(); if (uuids.length === 0) { alert(`请选择要执行“${opName}”的会话。`); return; } if (opName.includes('删除') && !confirm(`确定永久删除 ${uuids.length} 个会话吗?`)) return; document.querySelectorAll('.cpm-action-btn').forEach(btn => btn.disabled = true); this.updateStatus(`正在批量${opName} ${uuids.length} 个会话...`, 'info'); let successCount = 0; try { if (opName.includes('重命名')) { for (let i = 0; i < uuids.length; i++) { this.updateStatus(`正在${opName} ${i + 1}/${uuids.length}...`, 'info'); try { const newTitle = await serviceFunc(uuids[i]); const titleElement = document.querySelector(`li[data-uuid="${uuids[i]}"] .cpm-convo-title`); if (titleElement) { const star = titleElement.querySelector('.cpm-star'); titleElement.innerHTML = `${star ? star.outerHTML : ''}${newTitle}`; titleElement.style.color = 'hsl(var(--cpm-success-000))'; } successCount++; } catch (error) { const titleElement = document.querySelector(`li[data-uuid="${uuids[i]}"] .cpm-convo-title`); if(titleElement) { titleElement.style.color = 'hsl(var(--cpm-danger-000))'; } this.updateStatus(`第${i+1}个失败: ${error.message}`, 'error'); await new Promise(resolve => setTimeout(resolve, 1500)); } if (i < uuids.length - 1) await new Promise(resolve => setTimeout(resolve, 300)); } } else { successCount = await serviceFunc(uuids, ...args); } this.updateStatus(`操作完成。成功${opName} ${successCount}/${uuids.length} 个会话。`, 'success', 4000); } catch(e) { this.updateStatus(`批量${opName}失败: ${e.message}`, 'error', 5000); } const refreshSettingKey = opName.includes('删除') ? 'refreshAfterDelete' : opName.includes('收藏') ? 'refreshAfterStar' : 'refreshAfterRename'; if (GM_getValue(refreshSettingKey, false)) { this.updateStatus(document.querySelector('#cpm-main-panel .cpm-status-bar').textContent + ' 正在从服务器刷新列表...', 'info'); await this.loadConversations(); } else { this.renderConversationList(); } document.querySelectorAll('.cpm-action-btn').forEach(btn => btn.disabled = false); }, handleBatchRename() { this.handleBatchOperation('重命名', ManagerService.performAutoRename.bind(ManagerService)); }, handleBatchDelete() { this.handleBatchOperation('删除', ManagerService.performBatchDelete.bind(ManagerService)); }, handleBatchStar(isStarring) { this.handleBatchOperation(isStarring ? '收藏' : '取消收藏', ManagerService.performBatchStarAction.bind(ManagerService), isStarring); }, createExportSettingsHTML(forSettingsPanel = false) { const maybeRemoveTitle = forSettingsPanel ? '' : '<h3 class="cpm-setting-section-title">自定义导出默认设置</h3>'; return ` ${maybeRemoveTitle} <div class="cpm-setting-group" data-section="export-metadata"> <h4>基础信息</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-meta-include"><label for="cpm-export-meta-include">保留会话元数据</label></div> <div class="cpm-setting-sub-group" data-parent="meta-include"> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-meta-title"><label for="cpm-export-meta-title">标题 (name)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-meta-summary"><label for="cpm-export-meta-summary">摘要 (summary)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-meta-main-timestamps"><label for="cpm-export-meta-main-timestamps">会话创建/更新时间</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-meta-conv-settings"><label for="cpm-export-meta-conv-settings">会话设置 (settings)</label></div> </div> </div> <div class="cpm-setting-group" data-section="export-message"> <h4>消息结构</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-msg-sender"><label for="cpm-export-msg-sender">发送者 (sender)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-msg-uuids"><label for="cpm-export-msg-uuids">消息/父级UUID (建议保留)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-msg-other-meta"><label for="cpm-export-msg-other-meta">其他元数据 (index, stop_reason等)</label></div> </div> <div class="cpm-setting-group" data-section="export-timestamps"> <h4>时间戳信息</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-ts-message"><label for="cpm-export-ts-message">消息节点时间戳 (created_at/updated_at)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-ts-content"><label for="cpm-export-ts-content">内容块流式时间戳 (start/stop)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-ts-attachment"><label for="cpm-export-ts-attachment">附件创建时间戳</label></div> </div> <div class="cpm-setting-group" data-section="export-content"> <h4>核心内容</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-content-text"><label for="cpm-export-content-text">文本内容 (text块)</label></div> <div class="cpm-setting-item"> <label class="cpm-settings-label">附件信息:</label> <select id="cpm-export-attachments-mode"> <option value="full">完整信息 (含提取文本)</option> <option value="metadata_only">仅元数据 (文件名,大小等)</option> <option value="none">不保留附件</option> </select> </div> </div> <div class="cpm-setting-group" data-section="export-advanced"> <h4>高级内容</h4> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-thinking"><label for="cpm-export-adv-thinking">'思考'过程 (thinking块)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-tools-include"><label for="cpm-export-adv-tools-include">保留工具使用记录</label></div> <div class="cpm-setting-sub-group" data-parent="adv-tools-include"> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-tool-websearch"><label for="cpm-export-adv-tool-websearch">网页搜索 (web_search)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-tool-repl"><label for="cpm-export-adv-tool-repl">代码分析 (repl)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-tool-artifacts"><label for="cpm-export-adv-tool-artifacts">工件创建 (artifacts)</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-tool-other"><label for="cpm-export-adv-tool-other">其他未知工具</label></div> <div class="cpm-setting-item"><input type="checkbox" id="cpm-export-adv-tool-only-successful"><label for="cpm-export-adv-tool-only-successful">仅保留成功的工具调用</label></div> </div> </div> `; }, getExportSettings(container) { return { metadata: { include: container.querySelector('#cpm-export-meta-include').checked, title: container.querySelector('#cpm-export-meta-title').checked, summary: container.querySelector('#cpm-export-meta-summary').checked, main_timestamps: container.querySelector('#cpm-export-meta-main-timestamps').checked, conv_settings: container.querySelector('#cpm-export-meta-conv-settings').checked, }, message: { sender: container.querySelector('#cpm-export-msg-sender').checked, uuids: container.querySelector('#cpm-export-msg-uuids').checked, other_meta: container.querySelector('#cpm-export-msg-other-meta').checked, timestamps: { messageNode: container.querySelector('#cpm-export-ts-message').checked, contentBlock: container.querySelector('#cpm-export-ts-content').checked, attachment: container.querySelector('#cpm-export-ts-attachment').checked, } }, content: { text: container.querySelector('#cpm-export-content-text').checked, }, attachments: { mode: container.querySelector('#cpm-export-attachments-mode').value, }, advanced: { thinking: container.querySelector('#cpm-export-adv-thinking').checked, tools: { include: container.querySelector('#cpm-export-adv-tools-include').checked, web_search: container.querySelector('#cpm-export-adv-tool-websearch').checked, repl: container.querySelector('#cpm-export-adv-tool-repl').checked, artifacts: container.querySelector('#cpm-export-adv-tool-artifacts').checked, other: container.querySelector('#cpm-export-adv-tool-other').checked, onlySuccessful: container.querySelector('#cpm-export-adv-tool-only-successful').checked, } } }; }, loadExportSettings(container) { const prefix = 'exportDefault_'; const settings = { metadata: { include: GM_getValue(`${prefix}meta_include`, true), title: GM_getValue(`${prefix}meta_title`, true), summary: GM_getValue(`${prefix}meta_summary`, false), main_timestamps: GM_getValue(`${prefix}meta_main_timestamps`, false), conv_settings: GM_getValue(`${prefix}meta_conv_settings`, false), }, message: { sender: GM_getValue(`${prefix}msg_sender`, true), uuids: GM_getValue(`${prefix}msg_uuids`, true), other_meta: GM_getValue(`${prefix}msg_other_meta`, false), timestamps: { messageNode: GM_getValue(`${prefix}ts_message`, false), contentBlock: GM_getValue(`${prefix}ts_content`, false), attachment: GM_getValue(`${prefix}ts_attachment`, false), } }, content: { text: GM_getValue(`${prefix}content_text`, true) }, attachments: { mode: GM_getValue(`${prefix}attachments_mode`, 'full') }, advanced: { thinking: GM_getValue(`${prefix}adv_thinking`, true), tools: { include: GM_getValue(`${prefix}adv_tools_include`, true), web_search: GM_getValue(`${prefix}adv_tool_websearch`, true), repl: GM_getValue(`${prefix}adv_tool_repl`, true), artifacts: GM_getValue(`${prefix}adv_tool_artifacts`, true), other: GM_getValue(`${prefix}adv_tool_other`, true), onlySuccessful: GM_getValue(`${prefix}adv_tool_only_successful`, false), } } }; container.querySelector('#cpm-export-meta-include').checked = settings.metadata.include; container.querySelector('#cpm-export-meta-title').checked = settings.metadata.title; container.querySelector('#cpm-export-meta-summary').checked = settings.metadata.summary; container.querySelector('#cpm-export-meta-main-timestamps').checked = settings.metadata.main_timestamps; container.querySelector('#cpm-export-meta-conv-settings').checked = settings.metadata.conv_settings; container.querySelector('#cpm-export-msg-sender').checked = settings.message.sender; container.querySelector('#cpm-export-msg-uuids').checked = settings.message.uuids; container.querySelector('#cpm-export-msg-other-meta').checked = settings.message.other_meta; container.querySelector('#cpm-export-ts-message').checked = settings.message.timestamps.messageNode; container.querySelector('#cpm-export-ts-content').checked = settings.message.timestamps.contentBlock; container.querySelector('#cpm-export-ts-attachment').checked = settings.message.timestamps.attachment; container.querySelector('#cpm-export-content-text').checked = settings.content.text; container.querySelector('#cpm-export-attachments-mode').value = settings.attachments.mode; container.querySelector('#cpm-export-adv-thinking').checked = settings.advanced.thinking; container.querySelector('#cpm-export-adv-tools-include').checked = settings.advanced.tools.include; container.querySelector('#cpm-export-adv-tool-websearch').checked = settings.advanced.tools.web_search; container.querySelector('#cpm-export-adv-tool-repl').checked = settings.advanced.tools.repl; container.querySelector('#cpm-export-adv-tool-artifacts').checked = settings.advanced.tools.artifacts; container.querySelector('#cpm-export-adv-tool-other').checked = settings.advanced.tools.other; container.querySelector('#cpm-export-adv-tool-only-successful').checked = settings.advanced.tools.onlySuccessful; }, saveExportSettings(container) { const settings = this.getExportSettings(container); const prefix = 'exportDefault_'; GM_setValue(`${prefix}meta_include`, settings.metadata.include); GM_setValue(`${prefix}meta_title`, settings.metadata.title); GM_setValue(`${prefix}meta_summary`, settings.metadata.summary); GM_setValue(`${prefix}meta_main_timestamps`, settings.metadata.main_timestamps); GM_setValue(`${prefix}meta_conv_settings`, settings.metadata.conv_settings); GM_setValue(`${prefix}msg_sender`, settings.message.sender); GM_setValue(`${prefix}msg_uuids`, settings.message.uuids); GM_setValue(`${prefix}msg_other_meta`, settings.message.other_meta); GM_setValue(`${prefix}ts_message`, settings.message.timestamps.messageNode); GM_setValue(`${prefix}ts_content`, settings.message.timestamps.contentBlock); GM_setValue(`${prefix}ts_attachment`, settings.message.timestamps.attachment); GM_setValue(`${prefix}content_text`, settings.content.text); GM_setValue(`${prefix}attachments_mode`, settings.attachments.mode); GM_setValue(`${prefix}adv_thinking`, settings.advanced.thinking); GM_setValue(`${prefix}adv_tools_include`, settings.advanced.tools.include); GM_setValue(`${prefix}adv_tool_websearch`, settings.advanced.tools.web_search); GM_setValue(`${prefix}adv_tool_repl`, settings.advanced.tools.repl); GM_setValue(`${prefix}adv_tool_artifacts`, settings.advanced.tools.artifacts); GM_setValue(`${prefix}adv_tool_other`, settings.advanced.tools.other); GM_setValue(`${prefix}adv_tool_only_successful`, settings.advanced.tools.onlySuccessful); }, setupSubOptionDisabling(container) { const setupListener = (parentId, subGroupSelector) => { const parentCheckbox = container.querySelector(parentId); const subItems = container.querySelectorAll(subGroupSelector); if (!parentCheckbox || subItems.length === 0) return; const updateState = () => { const isDisabled = !parentCheckbox.checked; subItems.forEach(item => { item.querySelectorAll('input, select').forEach(el => el.disabled = isDisabled); item.classList.toggle('disabled', isDisabled); }); }; parentCheckbox.addEventListener('change', updateState); updateState(); }; setupListener('#cpm-export-meta-include', '.cpm-setting-sub-group[data-parent="meta-include"] .cpm-setting-item'); setupListener('#cpm-export-adv-tools-include', '.cpm-setting-sub-group[data-parent="adv-tools-include"] .cpm-setting-item'); }, showExportModal(uuid) { document.querySelector('.cpm-modal-overlay')?.remove(); const overlay = document.createElement('div'); overlay.className = 'cpm-modal-overlay'; const modalContent = document.createElement('div'); modalContent.className = 'cpm-panel cpm-export-modal-content'; modalContent.style.display = 'flex'; modalContent.innerHTML = ` <div class="cpm-header"><h2>自定义导出选项</h2><button class="cpm-close-button cpm-icon-btn">×</button></div> <div class="cpm-settings-content"> ${this.createExportSettingsHTML(false)} </div> <div class="cpm-settings-buttons"> <button id="cpm-export-now-btn" class="cpm-btn cpm-primary-btn">立即导出</button> </div> `; overlay.appendChild(modalContent); document.body.appendChild(overlay); this.loadExportSettings(modalContent); this.setupSubOptionDisabling(modalContent); overlay.onclick = (e) => { if(e.target === overlay) overlay.remove(); }; modalContent.querySelector('.cpm-close-button').onclick = () => overlay.remove(); modalContent.querySelector('#cpm-export-now-btn').onclick = async () => { const currentSettings = this.getExportSettings(modalContent); modalContent.querySelector('#cpm-export-now-btn').disabled = true; modalContent.querySelector('#cpm-export-now-btn').textContent = '正在导出...'; await ManagerService.performExportCustom(uuid, currentSettings, this.updateStatus.bind(this)); overlay.remove(); }; } }; // ========================================================================= // 8. 聊天增强模块 (Enhancer Modules) // ========================================================================= const BranchEnhancer = { state: { conversationUUID: null, selectedParentMessageUUID: null }, init() { this.cleanup(); this.createBranchButton(); }, updateState(currentUrl) { const pathParts = new URL(currentUrl).pathname.split('/'); this.state.conversationUUID = (pathParts[1] === 'chat' && pathParts[2]) ? pathParts[2] : null; if (!this.state.conversationUUID) this.state.selectedParentMessageUUID = null; this.updateStatusIndicator(); }, createBranchButton() { if (document.getElementById('cpm-branch-btn')) return; const toolbar = document.querySelector(Config.TOOLBAR_SELECTOR); if (!toolbar) return; const emptyArea = toolbar.querySelector(Config.EMPTY_AREA_SELECTOR); if (!emptyArea) return; const wrapperDiv = document.createElement('div'); wrapperDiv.className = "relative shrink-0"; const button = document.createElement('button'); button.id = 'cpm-branch-btn'; button.type = 'button'; button.title = '从对话历史的任意节点继续'; button.className = "inline-flex items-center justify-center relative shrink-0 can-focus select-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none border-0.5 transition-all h-8 min-w-8 rounded-lg flex items-center px-[7.5px] group !pointer-events-auto !outline-offset-1 text-text-300 border-border-300 active:scale-[0.98] hover:text-text-200/90 hover:bg-bg-100"; button.innerHTML = `<div class="flex flex-row items-center justify-center gap-1"><svg class="cpm-svg-icon" style="width:16px; height:16px; stroke-width:1.8;"><use href="#cpm-icon-tree"></use></svg></div>`; button.onclick = () => this.showModal(); wrapperDiv.appendChild(button); toolbar.insertBefore(wrapperDiv, emptyArea); }, async showModal() { const overlay = document.createElement('div'); overlay.className = 'cpm-modal-overlay'; overlay.onclick = () => overlay.remove(); const modalContent = document.createElement('div'); modalContent.className = 'cpm-panel cpm-tree-panel-override'; modalContent.style.display = 'flex'; modalContent.onclick = (e) => e.stopPropagation(); modalContent.innerHTML = ` <div class="cpm-header"><h2>选择一个分支起点</h2><button id="cpm-branch-modal-close-btn" class="cpm-icon-btn">×</button></div> <div id="cpm-branch-tree-container" class="cpm-tree-container"></div>`; overlay.appendChild(modalContent); document.body.appendChild(overlay); overlay.querySelector('#cpm-branch-modal-close-btn').onclick = () => overlay.remove(); const treeContainer = modalContent.querySelector('#cpm-branch-tree-container'); if (this.state.conversationUUID) { treeContainer.innerHTML = '<p class="cpm-loading">正在加载对话历史...</p>'; try { const historyData = await ClaudeAPI.getConversationHistory(this.state.conversationUUID); await SharedLogic.renderTreeView(treeContainer, historyData.chat_messages, { isForBranching: true, onNodeClick: (uuid, element) => this.selectBranchPoint(uuid, element) }); } catch (error) { treeContainer.innerHTML = `<p class="cpm-error">加载失败: ${error.message}</p>`; } } else { treeContainer.innerHTML = '<p class="cpm-loading">不在具体聊天内,无法从任何节点延续。</p>'; } }, selectBranchPoint(uuid, element) { this.state.selectedParentMessageUUID = uuid; document.querySelectorAll('.cpm-branch-node-selected').forEach(n => n.classList.remove('cpm-branch-node-selected')); element.classList.add('cpm-branch-node-selected'); this.updateStatusIndicator(); setTimeout(() => document.querySelector('.cpm-modal-overlay')?.remove(), 300); }, updateStatusIndicator() { const toolbar = document.querySelector(Config.TOOLBAR_SELECTOR); if (!toolbar) return; document.getElementById('cpm-branch-status-indicator')?.remove(); if (this.state.selectedParentMessageUUID) { const indicator = document.createElement('span'); indicator.id = 'cpm-branch-status-indicator'; indicator.textContent = '分支点已选定'; indicator.title = `下条消息将从指定节点开始。\nUUID: ${this.state.selectedParentMessageUUID}`; toolbar.appendChild(indicator); } }, cleanup() { document.querySelector('#cpm-branch-btn')?.closest('div.relative.shrink-0').remove(); document.getElementById('cpm-branch-status-indicator')?.remove(); } }; const AttachmentEnhancer = { state: { forceUploadMode: 'default', stagedAttachments: [], }, panelObserver: null, init() { this.cleanup(); this.createAttachmentPowerButton(); if (this.state.stagedAttachments.length > 0) { this.showPreviewPanel(); } }, createAttachmentPowerButton() { if (document.getElementById('cpm-attachment-power-btn')) return; const toolbar = document.querySelector(Config.TOOLBAR_SELECTOR); const emptyArea = toolbar?.querySelector(Config.EMPTY_AREA_SELECTOR); if (!toolbar || !emptyArea) return; const wrapperDiv = document.createElement('div'); wrapperDiv.className = "relative shrink-0"; wrapperDiv.innerHTML = ` <button class="inline-flex items-center justify-center relative shrink-0 can-focus select-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none border-0.5 transition-all h-8 min-w-8 rounded-lg flex items-center px-[7.5px] group !pointer-events-auto !outline-offset-1 text-text-300 border-border-300 active:scale-[0.98] hover:text-text-200/90 hover:bg-bg-100" type="button" id="cpm-attachment-power-btn" aria-label="打开PDF上传设置"> <div class="flex flex-row items-center justify-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg></div> </button> <div class="w-[24rem] absolute max-w-[calc(100vw-16px)] top-10 block hidden" id="cpm-attachment-power-menu"> <div class="relative w-full will-change-transform h-auto overflow-y-auto overscroll-auto flex z-dropdown bg-bg-000 rounded-lg overflow-hidden border-border-300 border-0.5 shadow-diffused shadow-[hsl(var(--always-black)/6%)] flex-col-reverse" style="max-height: 340px;"> <div class="flex flex-col min-h-0 w-full !ease-out justify-end" style="height: auto;"> <div class="w-full"> <div class="p-1.5 flex flex-col"> <button class="group flex w-full items-center text-left gap-2.5 py-auto px-1.5 text-[0.875rem] text-text-200 rounded-md transition-colors select-none active:!scale-100 hover:bg-bg-200/50 hover:text-text-000 h-[2rem]"> <div id="cpm-dynamic-icon-container" class="group/icon min-w-4 min-h-4 flex items-center justify-center text-text-300 shrink-0 group-hover:text-text-100"> <div id="cpm-icon-mode-off"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m6.75 12H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg></div> <div id="cpm-icon-mode-on" class="hidden"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg></div> </div> <div class="flex flex-col flex-1 min-w-0"><p class="text-[0.9375rem] text-text-300 group-hover:text-text-100">Force PDF Deep Analysis</p></div> <div class="flex items-center justify-center text-text-400" title="此功能为普通账户设计,可强制使用高级解析路径。Pro/Team账户原生支持,此开关对其无效。"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg></div> <div class="group/switch relative select-none cursor-pointer ml-2"> <input class="peer sr-only" type="checkbox" id="cpm-attachment-mode-toggle-switch"> <div class="border-border-300 rounded-full peer:can-focus peer-disabled:opacity-50 bg-bg-500 transition-colors peer-checked:bg-accent-secondary-100" style="width: 28px; height: 16px;"></div> <div class="absolute start-[2px] top-[2px] rounded-full transition-all peer-checked:translate-x-full rtl:peer-checked:-translate-x-full group-hover/switch:opacity-80 bg-white transition" style="height: 12px; width: 12px;"></div> </div> </button> </div> </div> </div> </div> </div>`; toolbar.insertBefore(wrapperDiv, emptyArea); this.setupEventListeners(); }, updateSubPanelIcon(isForceMode) { document.getElementById('cpm-icon-mode-off')?.classList.toggle('hidden', isForceMode); document.getElementById('cpm-icon-mode-on')?.classList.toggle('hidden', !isForceMode); }, setupEventListeners() { const triggerBtn = document.getElementById('cpm-attachment-power-btn'); const menu = document.getElementById('cpm-attachment-power-menu'); const toggleSwitch = document.getElementById('cpm-attachment-mode-toggle-switch'); if (!triggerBtn || !menu || !toggleSwitch) return; const isInitialForceMode = (this.state.forceUploadMode === 'force'); toggleSwitch.checked = isInitialForceMode; this.updateSubPanelIcon(isInitialForceMode); triggerBtn.addEventListener('click', (e) => { e.stopPropagation(); menu.classList.toggle('hidden'); }); const buttonInsideMenu = menu.querySelector('button.group'); buttonInsideMenu.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleSwitch.checked = !toggleSwitch.checked; toggleSwitch.dispatchEvent(new Event('change')); }); toggleSwitch.addEventListener('change', () => { const isForceMode = toggleSwitch.checked; this.state.forceUploadMode = isForceMode ? 'force' : 'default'; this.updateSubPanelIcon(isForceMode); console.log(LOG_PREFIX, `强制PDF深度解析模式已: ${isForceMode ? '开启' : '关闭'}`); }); document.addEventListener('click', (e) => { if (!menu.classList.contains('hidden') && !triggerBtn.contains(e.target) && !menu.contains(e.target)) { menu.classList.add('hidden'); } }); }, getOrCreatePreviewPanel() { let panel = document.getElementById(Config.ATTACHMENT_PANEL_ID); if (!panel) { panel = document.createElement('div'); panel.id = Config.ATTACHMENT_PANEL_ID; panel.innerHTML = ` <div class="cpm-attachment-panel-header"> <span>PDF深度解析暂存区</span> <button class="cpm-icon-btn cpm-attachment-panel-close-btn" title="关闭并清空所有暂存文件"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"></path></svg> </button> </div> <div class="cpm-attachment-panel-content"></div>`; document.body.appendChild(panel); panel.querySelector('.cpm-attachment-panel-close-btn').onclick = () => this.clearAndHidePanel(); panel.addEventListener('click', (e) => { const deleteBtn = e.target.closest('.cpm-preview-delete-btn'); if (!deleteBtn) return; e.preventDefault(); e.stopPropagation(); const uuidToDelete = deleteBtn.dataset.uuid; this.removeStagedFile(uuidToDelete); }); this.panelObserver = new MutationObserver((mutations) => { if (!document.getElementById(Config.ATTACHMENT_PANEL_ID)) { this.clearStagedFiles(); this.panelObserver.disconnect(); this.panelObserver = null; console.log(LOG_PREFIX, "暂存面板已从DOM移除,自动清空暂存文件。"); } }); this.panelObserver.observe(document.body, { childList: true }); } return panel; }, showPreviewPanel() { const panel = this.getOrCreatePreviewPanel(); void panel.offsetWidth; panel.classList.add('visible'); }, hidePreviewPanel() { const panel = document.getElementById(Config.ATTACHMENT_PANEL_ID); if (panel) { panel.classList.remove('visible'); const transitionEndHandler = () => { if (!panel.classList.contains('visible')) { panel.remove(); } panel.removeEventListener('transitionend', transitionEndHandler); }; panel.addEventListener('transitionend', transitionEndHandler); } }, addFileToPanel(fileInfo) { this.showPreviewPanel(); const content = this.getOrCreatePreviewPanel().querySelector('.cpm-attachment-panel-content'); if (!content) return; const previewUrl = `/api/${fileInfo.org_uuid}/files/${fileInfo.uuid}/document_pdf/${encodeURIComponent(fileInfo.fileName)}`; const wrapper = document.createElement('div'); wrapper.className = 'cpm-preview-thumbnail-wrapper'; wrapper.id = `thumbnail-wrapper-${fileInfo.uuid}`; wrapper.innerHTML = ` <button class="cpm-preview-delete-btn" data-uuid="${fileInfo.uuid}" title="移除文件"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 256 256"><path d="M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"></path></svg> </button> <a href="${previewUrl}" target="_blank" rel="noopener noreferrer" class="cpm-preview-thumbnail-link" title="点击预览: ${fileInfo.fileName}"> <img src="${fileInfo.thumbnailUrl}" alt="${fileInfo.fileName}"> <div class="cpm-preview-thumbnail-overlay"> <p class="cpm-preview-thumbnail-name">${fileInfo.fileName}</p> </div> </a>`; content.appendChild(wrapper); }, clearStagedFiles() { if (this.state.stagedAttachments.length > 0) { console.log(LOG_PREFIX, `正在清空 ${this.state.stagedAttachments.length} 个暂存文件。`); this.state.stagedAttachments = []; } }, clearAndHidePanel() { this.clearStagedFiles(); this.hidePreviewPanel(); }, removeStagedFile(uuid) { const index = this.state.stagedAttachments.findIndex(f => f.uuid === uuid); if (index > -1) { const fileName = this.state.stagedAttachments[index].fileName; this.state.stagedAttachments.splice(index, 1); console.log(LOG_PREFIX, `文件已从暂存区移除: ${fileName}`); document.getElementById(`thumbnail-wrapper-${uuid}`)?.remove(); if (this.state.stagedAttachments.length === 0) { this.hidePreviewPanel(); } } }, schedulePanelClosure(delay = 3000) { setTimeout(() => { const panel = document.getElementById(Config.ATTACHMENT_PANEL_ID); if (panel) this.hidePreviewPanel(); }, delay); }, shouldForceUpload(fileName) { if (!fileName || typeof fileName !== 'string') return false; const ext = ('.' + fileName.split('.').pop()).toLowerCase(); return Config.FORCE_UPLOAD_TARGET_EXTENSIONS.includes(ext) && this.state.forceUploadMode === 'force'; }, cleanup() { document.querySelector('#cpm-attachment-power-btn')?.closest('div.relative.shrink-0').remove(); this.hidePreviewPanel(); if (this.panelObserver) { this.panelObserver.disconnect(); this.panelObserver = null; } } }; // ========================================================================= // 9. 核心拦截与启动模块 // ========================================================================= const App = { lastUrl: '', observer: null, init() { ThemeManager.init(); this.installFetchInterceptor(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.startObserver()); } else { this.startObserver(); } }, installFetchInterceptor() { const originalFetch = window.fetch; window.fetch = async function(...args) { let url = args[0] instanceof Request ? args[0].url : String(args[0]); let options = args[1] || {}; if (url.includes('/convert_document') && options.body instanceof FormData) { const file = Array.from(options.body.values()).find(v => v instanceof File); if (file && AttachmentEnhancer.shouldForceUpload(file.name)) { console.groupCollapsed(`%c${LOG_PREFIX} [劫持] 强制PDF深度解析...`, 'color: #ef4444; font-weight: bold;'); const orgUuidMatch = url.match(/\/api\/organizations\/(.*?)\/convert_document/); if (orgUuidMatch) { const org_uuid = orgUuidMatch[1]; const uploadUrl = `/api/${org_uuid}/upload`; originalFetch(uploadUrl, options) .then(res => res.ok ? res.json() : Promise.reject(`后台上传失败: ${res.statusText}`)) .then(uploadResult => { if (uploadResult.file_uuid && uploadResult.thumbnail_asset?.url) { const fileInfo = { uuid: uploadResult.file_uuid, fileName: uploadResult.file_name, org_uuid: org_uuid, thumbnailUrl: uploadResult.thumbnail_asset.url }; AttachmentEnhancer.state.stagedAttachments.push(fileInfo); AttachmentEnhancer.addFileToPanel(fileInfo); console.log('后台 /upload 强制上传成功并已暂存:', fileInfo.fileName); } }).catch(error => console.error(`${LOG_PREFIX} 后台 /upload 任务失败:`, error)) .finally(() => console.groupEnd()); } else { console.error(`${LOG_PREFIX} 无法从URL中提取组织UUID。`); console.groupEnd(); } return Promise.resolve(new Response(JSON.stringify({}), { status: 200, statusText: "OK (Handled by Enhancer)" })); } } if (url.includes('/completion') && (AttachmentEnhancer.state.stagedAttachments.length > 0 || BranchEnhancer.state.selectedParentMessageUUID)) { console.groupCollapsed(`%c${LOG_PREFIX} 请求注入: 正在处理/completion...`, 'color: #8b5cf6; font-weight: bold;'); if (options.body && typeof options.body === 'string') { try { const payload = JSON.parse(options.body); if (AttachmentEnhancer.state.stagedAttachments.length > 0) { console.log(`执行附件注入... (${AttachmentEnhancer.state.stagedAttachments.length}个文件)`); const hijackedFileNames = AttachmentEnhancer.state.stagedAttachments.map(att => att.fileName); if (payload.attachments) { payload.attachments = payload.attachments.filter(att => !hijackedFileNames.includes(att.file_name)); } const fileUuidsToInject = AttachmentEnhancer.state.stagedAttachments.map(att => att.uuid); if (!payload.files) payload.files = []; fileUuidsToInject.forEach(uuid => { if (!payload.files.includes(uuid)) payload.files.push(uuid); }); AttachmentEnhancer.clearStagedFiles(); AttachmentEnhancer.schedulePanelClosure(); console.log("附件注入完成,暂存区已清空。"); } if (BranchEnhancer.state.selectedParentMessageUUID) { console.log("执行分支注入..."); payload.parent_message_uuid = BranchEnhancer.state.selectedParentMessageUUID; BranchEnhancer.state.selectedParentMessageUUID = null; setTimeout(() => BranchEnhancer.updateStatusIndicator(), 0); console.log("分支注入完成。"); } options.body = JSON.stringify(payload); } catch (e) { console.error(LOG_PREFIX, "修改/completion请求体失败:", e); } finally { console.groupEnd(); } } } return originalFetch.apply(this, args); }; }, startObserver() { this.observer = new MutationObserver(() => this.onPageChange()); this.observer.observe(document.body, { childList: true, subtree: true }); this.onPageChange(); }, onPageChange() { const currentUrl = location.href; if (currentUrl === this.lastUrl && document.getElementById('cpm-manager-button')) { if(document.querySelector(Config.TOOLBAR_SELECTOR) && !document.getElementById('cpm-branch-btn')) { this.setupEnhancers(currentUrl); } return; } this.lastUrl = currentUrl; console.log(LOG_PREFIX, "URL变更或初次加载,执行页面设置。"); ManagerUI.init(); this.setupEnhancers(currentUrl); if (AttachmentEnhancer.state.stagedAttachments.length > 0) { AttachmentEnhancer.showPreviewPanel(); } else { AttachmentEnhancer.hidePreviewPanel(); } }, setupEnhancers(currentUrl) { const toolbar = document.querySelector(Config.TOOLBAR_SELECTOR); if (toolbar) { BranchEnhancer.init(); AttachmentEnhancer.init(); BranchEnhancer.updateState(currentUrl); } else { BranchEnhancer.cleanup(); AttachmentEnhancer.cleanup(); } } }; // ========================================================================= // 10. CSS 样式 (全部整合) // ========================================================================= GM_addStyle(` /* --- THEME VARIABLES --- */ body[cpm-theme='light'] { --cpm-bg-000: 0 0% 100%; --cpm-bg-100: 48 33.3% 97.1%; --cpm-bg-200: 53 28.6% 94.5%; --cpm-bg-300: 48 25% 92.2%; --cpm-bg-400: 50 20.7% 88.6%; --cpm-bg-500: 50 20.7% 88.6%; --cpm-text-000: 60 2.6% 7.6%; --cpm-text-100: 60 2.6% 7.6%; --cpm-text-200: 60 2.5% 23.3%; --cpm-text-300: 60 2.5% 23.3%; --cpm-text-400: 51 3.1% 43.7%; --cpm-text-500: 51 3.1% 43.7%; --cpm-border-100: 30 3.3% 11.8%; --cpm-border-200: 30 3.3% 11.8%; --cpm-border-300: 45 8.3% 84.1%; --cpm-border-400: 30 3.3% 11.8%; --cpm-accent-brand: 15 63.1% 59.6%; --cpm-accent-secondary-100: 210 70.9% 51.6%; --cpm-accent-pro-100: 251 40% 45.1%; --cpm-danger-000: 0 72.2% 50.6%; --cpm-danger-100: 0 58.6% 34.1%; --cpm-success-000: 145 58% 34%; --cpm-oncolor-100: 0 0% 100%; --cpm-highlight-orange: 31 56% 61%; --cpm-brand-orange-base: 19 58% 55%; --cpm-always-black: 0 0% 0%; --cpm-sender-you-color: #15803d; --cpm-sender-claude-color: #1d4ed8; --cpm-branch-hover-bg: rgba(93, 93, 255, 0.2); --cpm-branch-selected-bg: #43a047; --cpm-branch-selected-text: white; } body[cpm-theme='dark'] { --cpm-bg-000: 60 2.1% 18.4%; --cpm-bg-100: 60 2.7% 14.5%; --cpm-bg-200: 30 3.3% 11.8%; --cpm-bg-300: 60 2.6% 7.6%; --cpm-bg-400: 60 3.4% 5.7%; --cpm-bg-500: 60 3.4% 5.7%; --cpm-text-000: 48 33.3% 97.1%; --cpm-text-100: 48 33.3% 97.1%; --cpm-text-200: 50 9% 73.7%; --cpm-text-300: 50 9% 73.7%; --cpm-text-400: 48 4.8% 59.2%; --cpm-text-500: 48 4.8% 59.2%; --cpm-border-100: 51 16.5% 84.5%; --cpm-border-200: 51 16.5% 84.5%; --cpm-border-300: 51 16.5% 84.5%; --cpm-border-400: 51 16.5% 84.5%; --cpm-accent-brand: 15 63.1% 59.6%; --cpm-accent-secondary-100: 210 70.9% 51.6%; --cpm-accent-pro-100: 251 40.2% 54.1%; --cpm-danger-000: 0 73.1% 66.5%; --cpm-danger-100: 0 58.6% 34.1%; --cpm-success-000: 145 63% 52%; --cpm-oncolor-100: 0 0% 100%; --cpm-highlight-orange: 31 56% 61%; --cpm-brand-orange-base: 19 58% 55%; --cpm-always-black: 0 0% 0%; --cpm-sender-you-color: #81c784; --cpm-sender-claude-color: #82aaff; --cpm-branch-hover-bg: rgba(93, 93, 255, 0.4); --cpm-branch-selected-bg: #2a9d8f; --cpm-branch-selected-text: white; } /* --- SHARED & BASE --- */ .cpm-svg-icon { width: 1.1em; height: 1.1em; display: inline-block; vertical-align: middle; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } #cpm-manager-button { position: fixed; bottom: 18px; right: 18px; z-index: 9998; background-color: hsl(var(--cpm-brand-orange-base)); color: hsl(var(--cpm-oncolor-100)); border: none; border-radius: 8px; padding: 4px 8px; font-size: 16px; font-weight: 600; font-family: sans-serif; cursor: pointer; letter-spacing: 0.2px; box-shadow: 0 4px 12px hsla(var(--cpm-text-000), 0.15); transition: all 0.2s ease-in-out; } #cpm-manager-button:hover { box-shadow: 0 8px 20px hsla(var(--cpm-text-000), 0.2); transform: scale(1.05) rotate(-1deg); } #cpm-manager-button:active { box-shadow: 0 2px 5px hsla(var(--cpm-text-000), 0.15); transform: scale(0.98); transition-duration: 0.1s; } /* --- PANELS & MODALS --- */ .cpm-panel { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80vw; max-width: 800px; height: 80vh; background-color: hsl(var(--cpm-bg-100)); color: hsl(var(--cpm-text-200)); border: 1px solid hsl(var(--cpm-border-300)); border-radius: 12px; z-index: 9999; box-shadow: 0 10px 25px hsla(var(--cpm-text-000), 0.2); flex-direction: column; font-family: sans-serif; transition: background-color 0.3s, color 0.3s; } .cpm-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: hsla(var(--cpm-text-000), 0.7); display: flex; justify-content: center; align-items: center; z-index: 10000; } .cpm-export-modal-content { max-width: 600px; height: auto; max-height: 90vh; } .cpm-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-bottom: 1px solid hsl(var(--cpm-border-200)); flex-shrink: 0; } .cpm-header h2 { margin: 0; font-size: 18px; color: hsl(var(--cpm-text-100)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cpm-header-actions { display: flex; align-items: center; gap: 8px; } .cpm-icon-btn { background: none; border: none; color: hsl(var(--cpm-text-400)); font-size: 1.1em; cursor: pointer; padding: 4px; border-radius: 4px; transition: color 0.2s, background-color 0.2s; line-height: 1; display:flex; align-items:center; justify-content:center; } .cpm-icon-btn:hover { color: hsl(var(--cpm-text-100)); background-color: hsl(var(--cpm-bg-200)); } /* --- MANAGER UI --- */ .cpm-toolbar { display: flex; flex-wrap: wrap; gap: 15px; padding: 12px 20px; background-color: hsl(var(--cpm-bg-200)); border-bottom: 1px solid hsl(var(--cpm-border-200)); align-items: center; flex-shrink: 0; } .cpm-toolbar-group { display: flex; align-items: center; gap: 8px; } .cpm-toolbar input, .cpm-toolbar select { background-color: hsl(var(--cpm-bg-000)); color: hsl(var(--cpm-text-100)); border: 1px solid hsl(var(--cpm-border-300)); border-radius: 4px; padding: 4px 8px; } .cpm-btn, .cpm-action-btn { background-color: hsl(var(--cpm-bg-400)); color: hsl(var(--cpm-text-100)); border: 1px solid hsl(var(--cpm-border-300)); border-radius: 6px; padding: 4px 10px; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; } .cpm-btn:hover, .cpm-action-btn:hover { background-color: hsl(var(--cpm-bg-500)); } .cpm-actions { display: flex; flex-wrap: wrap; gap: 10px; padding: 12px 20px; align-items: center; flex-shrink: 0; } .cpm-action-btn { padding: 8px 14px; } .cpm-action-btn:disabled { background-color: hsl(var(--cpm-bg-300)); cursor: not-allowed; opacity: 0.6; } .cpm-danger-btn { background-color: hsla(var(--cpm-danger-100), 0.8); border-color: hsl(var(--cpm-danger-100)); } .cpm-danger-btn:hover { background-color: hsl(var(--cpm-danger-100)); } #cpm-refresh { margin-left: auto; } .cpm-list-container { flex-grow: 1; overflow-y: auto; padding: 0 5px 0 20px; border-top: 1px solid hsl(var(--cpm-border-200)); } .cpm-loading, .cpm-error, .cpm-list-container p { color: hsl(var(--cpm-text-300)); text-align: center; margin-top: 20px; display: flex; align-items: center; justify-content: center; gap: 8px; } .cpm-convo-list { list-style: none; padding: 0; margin: 0; } .cpm-convo-list li { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid hsl(var(--cpm-border-200)); transition: background-color 0.2s; } .cpm-convo-list li:not(.is-editing):hover { background-color: hsl(var(--cpm-bg-200)); } .cpm-checkbox { margin-right: 15px; width: 16px; height: 16px; cursor: pointer; flex-shrink: 0; } .cpm-convo-details { display: flex; flex-direction: column; gap: 4px; flex-grow: 1; min-width: 0; } .cpm-convo-title { font-size: 15px; color: hsl(var(--cpm-text-100)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 0.3s ease; } .cpm-star { color: #facc15; margin-right: 5px; } .cpm-convo-date { font-size: 12px; color: hsl(var(--cpm-text-400)); } .cpm-convo-actions { display: flex; gap: 5px; padding: 0 10px; } .cpm-action-save { color: hsl(var(--cpm-success-000)) !important; } .cpm-action-cancel { color: hsl(var(--cpm-danger-000)) !important; } .cpm-status-bar { padding: 8px 20px; border-top: 1px solid hsl(var(--cpm-border-200)); font-size: 12px; color: hsl(var(--cpm-text-400)); text-align: right; flex-shrink: 0; transition: color 0.3s; } .cpm-status-bar.is-error { color: hsl(var(--cpm-danger-000)); } .cpm-status-bar.is-success { color: hsl(var(--cpm-success-000)); } .cpm-highlight { color: hsl(var(--cpm-accent-brand)); font-weight: bold; background-color: hsla(var(--cpm-accent-brand), 0.1); } .cpm-edit-input { width: 100%; background-color: hsl(var(--cpm-bg-200)); border: 1px solid hsl(var(--cpm-border-300)); border-radius: 4px; color: hsl(var(--cpm-text-100)); padding: 4px 8px; font-size: 15px; line-height: 1.5; box-sizing: border-box; } .cpm-edit-input:focus { outline: none; border-color: hsl(var(--cpm-accent-brand)); } li.is-editing .cpm-convo-details { padding-top: 2px; padding-bottom: 2px; } /* --- SETTINGS PANEL --- */ .cpm-settings-content { padding: 20px; overflow-y: auto; background-color: hsl(var(--cpm-bg-000)); flex-grow: 1; } .cpm-setting-section { margin-bottom: 25px; border-bottom: 1px solid hsl(var(--cpm-border-200)); padding-bottom: 15px; } .cpm-setting-section:last-of-type { border-bottom: none; } .cpm-setting-section-title { margin-top: 0; padding-bottom: 15px; color: hsl(var(--cpm-text-100)); font-size: 16px; font-weight: 600; } .cpm-setting-group { margin-bottom: 15px; } .cpm-setting-group h4 { color: hsl(var(--cpm-text-300)); font-size: 14px; margin-bottom: 10px; } .cpm-setting-sub-group { padding-left: 20px; border-left: 2px solid hsl(var(--cpm-bg-200)); margin-top: 10px; } .cpm-setting-item { display: flex; align-items: center; gap: 15px; margin-bottom: 12px; } .cpm-setting-item label { color: hsl(var(--cpm-text-200)); cursor: pointer; } .cpm-settings-label { width: 150px; text-align: right; flex-shrink: 0; } .cpm-setting-item input[type="text"], .cpm-setting-item input[type="number"], .cpm-setting-item select { background-color: hsl(var(--cpm-bg-100)); border: 1px solid hsl(var(--cpm-border-300)); color: hsl(var(--cpm-text-100)); border-radius: 4px; padding: 8px; flex-grow: 1; } .cpm-setting-item input[type="checkbox"] { width: 16px; height: 16px; } .cpm-setting-item.disabled { opacity: 0.5; } .cpm-setting-item.disabled label { cursor: not-allowed; } .cpm-settings-buttons { display: flex; justify-content: center; gap: 20px; margin-top: 30px; } .cpm-settings-buttons .cpm-btn { padding: 10px 20px; color: hsl(var(--cpm-oncolor-100)); border: none; border-radius: 6px; cursor: pointer; } #cpm-back-to-main { background-color: hsl(var(--cpm-bg-400)); } #cpm-save-settings-button, #cpm-export-now-btn { background-color: hsl(var(--cpm-accent-secondary-100)); } #cpm-export-now-btn:disabled { background-color: hsl(var(--cpm-bg-300)); cursor: not-allowed; } /* --- TREE VIEW --- */ .cpm-tree-panel-override { width: 90vw; max-width: 1200px; height: 90vh; } .cpm-tree-container { flex-grow: 1; overflow-y: auto; padding: 20px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 14px; background-color: hsl(var(--cpm-bg-200)); } .cpm-tree-node { margin-bottom: 10px; border-radius: 6px; } .cpm-tree-node-header { margin: 0 0 5px 0; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; padding: 4px; } .cpm-tree-node-id { color: hsl(var(--cpm-text-400)); font-size: 12px; flex-shrink: 0; } .cpm-tree-node-sender { font-weight: bold; flex-shrink: 0; } .sender-you { color: var(--cpm-sender-you-color); } .sender-claude { color: var(--cpm-sender-claude-color); } .cpm-tree-node-preview { color: hsl(var(--cpm-text-200)); word-break: break-all; } .cpm-tree-attachments { color: hsl(var(--cpm-text-300)); font-size: 12px; padding-left: 20px; } .cpm-tree-attachments ul { list-style: none; padding-left: 10px; margin: 5px 0 0 0; } .cpm-tree-attachments li { margin-bottom: 4px; } .cpm-attachment-source { color: hsl(var(--cpm-accent-pro-100)); margin: 0 5px; font-style: italic; } .cpm-attachment-details { color: hsl(var(--cpm-text-400)); } .cpm-attachment-url { color: hsl(var(--cpm-accent-secondary-100)); text-decoration: none; } .cpm-attachment-url:hover { text-decoration: underline; } /* --- ENHANCER-SPECIFIC STYLES --- */ #cpm-branch-status-indicator { background-color: var(--cpm-branch-selected-bg); color: var(--cpm-branch-selected-text); padding: 2px 8px; font-size: 12px; border-radius: 12px; margin-left: 8px; font-weight: 500; animation: cpm-fadeIn 0.3s ease; } @keyframes cpm-fadeIn { from { opacity: 0; } to { opacity: 1; } } #cpm-branch-from-root-btn { border: 1px dashed hsl(var(--cpm-border-300)); padding: 10px; margin-bottom: 20px; text-align: center; font-weight: bold; color: hsl(var(--cpm-text-200)); border-radius: 6px; cursor: pointer; transition: all 0.2s; } .cpm-branch-node-clickable { cursor: pointer; transition: background-color 0.2s; } .cpm-branch-node-clickable:hover, #cpm-branch-from-root-btn:hover { background-color: var(--cpm-branch-hover-bg); } .cpm-branch-node-selected, #cpm-branch-from-root-btn.cpm-branch-node-selected { background-color: var(--cpm-branch-selected-bg) !important; color: var(--cpm-branch-selected-text) !important; } .cpm-branch-node-selected .cpm-tree-node-sender, .cpm-branch-node-selected .cpm-tree-node-preview, .cpm-branch-node-selected .cpm-tree-node-id { color: var(--cpm-branch-selected-text) !important; } #cpm-attachment-power-menu .bg-bg-000 { background-color: hsl(var(--cpm-bg-000)); } #cpm-attachment-power-menu .text-text-200 { color: hsl(var(--cpm-text-200)); } #cpm-attachment-power-menu .text-text-300 { color: hsl(var(--cpm-text-300)); } #cpm-attachment-power-menu .hover\\:bg-bg-200\\/50:hover { background-color: hsl(var(--cpm-bg-200) / 0.5); } #cpm-attachment-power-menu .hover\\:text-text-000:hover { color: hsl(var(--cpm-text-000)); } #cpm-attachment-power-menu .group-hover\\:text-text-100:hover { color: hsl(var(--cpm-text-100)); } #cpm-attachment-power-menu .bg-bg-500 { background-color: hsl(var(--cpm-bg-500)); } #cpm-attachment-mode-toggle-switch:checked + div { background-color: hsl(var(--cpm-accent-secondary-100)) !important; } /* --- ATTACHMENT PREVIEW PANEL --- */ #cpm-attachment-preview-panel { position: fixed; right: 20px; bottom: 80px; width: 320px; max-height: 480px; background-color: hsl(var(--cpm-bg-100)); border: 0.5px solid hsl(var(--cpm-border-300)); border-radius: 12px; box-shadow: 0 10px 25px -5px hsla(var(--cpm-always-black), 0.1), 0 8px 10px -6px hsla(var(--cpm-always-black), 0.1); z-index: 9999; display: flex; flex-direction: column; overflow: hidden; opacity: 0; transform: translateY(20px); transition: opacity 0.4s ease-out, transform 0.4s ease-out; pointer-events: none; } #cpm-attachment-preview-panel.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } .cpm-attachment-panel-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 8px 8px 12px; font-weight: 600; font-size: 14px; color: hsl(var(--cpm-text-300)); border-bottom: 0.5px solid hsl(var(--cpm-border-200)); flex-shrink: 0; } .cpm-attachment-panel-content { padding: 12px; display: flex; flex-wrap: wrap; gap: 12px; overflow-y: auto; justify-content: center; } .cpm-preview-thumbnail-wrapper { position: relative; transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; border-radius: 8px; box-shadow: 0 1px 3px 0 hsla(var(--cpm-always-black), 0.08); } .cpm-preview-thumbnail-wrapper:hover { transform: scale(1.04); z-index: 10; box-shadow: 0 8px 16px hsla(var(--cpm-always-black), 0.15); } .cpm-preview-thumbnail-link { display: block; width: 112px; height: 160px; border-radius: 8px; overflow: hidden; border: 0.5px solid hsla(var(--cpm-border-300), 0.5); text-decoration: none; position: relative; background-color: hsl(var(--cpm-bg-300)); } .cpm-preview-thumbnail-link img { width: 100%; height: 100%; object-fit: cover; } .cpm-preview-thumbnail-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, hsla(var(--cpm-always-black), 0.8), transparent); padding: 12px 6px 6px; text-align: center; } .cpm-preview-thumbnail-name { color: white; font-size: 12px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cpm-preview-delete-btn { position: absolute; top: -8px; left: -8px; width: 20px; height: 20px; background-color: hsla(var(--cpm-bg-000), 0.9); color: hsl(var(--cpm-text-400)); border: 0.5px solid hsla(var(--cpm-border-200), 0.25); border-radius: 50%; backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transform: scale(0.8); transition: opacity 0.2s ease, transform 0.2s ease, background-color 0.2s ease, color 0.2s ease; z-index: 20; } .cpm-preview-thumbnail-wrapper:hover .cpm-preview-delete-btn { opacity: 1; transform: scale(1); } .cpm-preview-delete-btn:hover { background-color: hsla(var(--cpm-bg-200), 0.95); color: hsl(var(--cpm-text-100)); } .cpm-preview-delete-btn svg { width: 12px; height: 12px; } `); // ========================================================================= // 11. 辅助工具 & 启动脚本 // ========================================================================= String.prototype.rsplit = function(sep, maxsplit) { const split = this.split(sep); return maxsplit ? [ split.slice(0, -maxsplit).join(sep) ].concat(split.slice(-maxsplit)) : split; }; App.init(); })(unsafeWindow);