// ==UserScript== // @name Claude Powerest Manager Enhancer | 导航 导出 管理 跳转 分支 对话 管理器 导出器 export navigate jump branch helper // @name:zh-CN Claude神级拓展增强脚本 | (管理 增强 导出 导航 跳转 分支 分叉 管理器 增强器 导出器 导航器 助手) | (manage enhance export navigate jump branch fork manager enhancer exporter navigator helper) // @namespace http://tampermonkey.net/ // @version 1.2.4 // @description 一站式搜索、筛选、批量管理所有对话。强大的JSON导出(原始/自定义/含附件)。为聊天框注入新功能,如从任意消息分支、跨分支全局导航、强制PDF深度解析、浮动线性导航面板等。关键词: 管理 增强 导出 导航 跳转 分支 分叉 管理器 增强器 导出器 导航器 助手 manage enhance export navigate jump branch fork manager enhancer exporter navigator helper // @description:zh-CN [管理器] 右下角打开管理器面板开启一站式搜索、筛选、批量管理所有对话。强大的JSON导出(原始/自定义/含附件)。[增强器]为聊天框注入新功能,如从任意消息分支、跨分支全局导航、强制PDF深度解析、浮动线性导航面板等。 // @description:en [Manager] Opens a management panel in the bottom-right corner for one-stop searching, filtering, and batch management of all conversations. Powerful JSON export (raw/custom/with attachments). [Enhancer] Injects new features into the chat interface, such as branching from any message, cross-branch navigation, forced deep PDF parsing, floating linear navigation panel, and more. // @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-idle // @downloadURL https://update.greasyfork.icu/scripts/539886/Claude%20Powerest%20Manager%20Enhancer%20%7C%20%E5%AF%BC%E8%88%AA%20%E5%AF%BC%E5%87%BA%20%E7%AE%A1%E7%90%86%20%E8%B7%B3%E8%BD%AC%20%E5%88%86%E6%94%AF%20%E5%AF%B9%E8%AF%9D%20%E7%AE%A1%E7%90%86%E5%99%A8%20%E5%AF%BC%E5%87%BA%E5%99%A8%20export%20navigate%20jump%20branch%20helper.user.js // @updateURL https://update.greasyfork.icu/scripts/539886/Claude%20Powerest%20Manager%20Enhancer%20%7C%20%E5%AF%BC%E8%88%AA%20%E5%AF%BC%E5%87%BA%20%E7%AE%A1%E7%90%86%20%E8%B7%B3%E8%BD%AC%20%E5%88%86%E6%94%AF%20%E5%AF%B9%E8%AF%9D%20%E7%AE%A1%E7%90%86%E5%99%A8%20%E5%AF%BC%E5%87%BA%E5%99%A8%20export%20navigate%20jump%20branch%20helper.meta.js // ==/UserScript== (function(window) { 'use strict'; const LOG_PREFIX = "[ClaudePowerestManager&Enhancer v1.2.4]:" console.log(LOG_PREFIX, "脚本已加载。"); // 全局HTML转义函数 - 统一的转义实现 function escapeHTML(str) { if (!str) return ''; return str.replace(/[&<>"']/g, function(match) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[match]; }); } // 全局字符串分割工具函数 - 避免原型污染 function rsplit(str, sep, maxsplit) { const split = str.split(sep); return maxsplit ? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit)) : split; } // ========================================================================= // 0. 国际化配置和翻译函数 // ========================================================================= const I18N_CONFIG = { currentLang: GM_getValue('language', 'zh'), translations: { zh: { // Manager 相关 'manager.title': 'Manager', 'manager.refresh': '刷新列表', 'manager.selectAll': '全选', 'manager.selectNone': '全不选', 'manager.selectInvert': '反选', 'manager.batchStar': '批量收藏', 'manager.batchUnstar': '批量取消收藏', 'manager.batchRename': '批量自动重命名', 'manager.batchDelete': '批量删除', 'manager.loading': '正在加载会话列表...', 'manager.noResults': '没有符合条件的会话。', 'manager.ready': '准备就绪。', 'manager.refreshButtonTip': '点击刷新按钮 ( ) 加载会话列表。', // Settings 相关 'settings.title': '管理器设置', 'settings.theme': '外观设置', 'settings.themeMode': '脚本主题:', 'settings.themeAuto': '跟随网站', 'settings.themeLight': '锁定白天', 'settings.themeDark': '锁定黑夜', 'settings.batchOps': '批量操作设置', 'settings.exportDefaults': '自定义导出默认设置', 'settings.save': '保存设置', 'settings.saved': '设置已保存!', 'settings.backToMain': '返回主面板', 'settings.language': '语言设置', 'settings.interfaceLanguage': '界面语言:', // Navigator 相关 'navigator.title': '对话节点延续&导航器', 'navigator.branchMode': '延续模式', 'navigator.navigateMode': '导航模式', 'navigator.branchSelected': '分支点已选定', 'navigator.loading': '正在加载对话历史...', 'navigator.branchFromRoot': '从根节点开始 (创建一个新的主分支)', // Linear Navigator 相关 'linear.title': '线性导航', 'linear.refresh': '刷新对话列表', 'linear.close': '关闭线性导航', 'linear.top': '回到顶部', 'linear.bottom': '回到底部', 'linear.prev': '上一条', 'linear.next': '下一条', 'linear.empty': '暂无线性对话', // Attachment 相关 'attachment.title': 'PDF深度解析暂存区', 'attachment.forceMode': 'Force PDF Deep Analysis', 'attachment.close': '关闭并清空所有暂存文件', // Export 相关 'export.original': '原始JSON导出', 'export.custom': '自定义JSON导出', 'export.batchOriginal': '批量原始JSON导出', 'export.batchCustom': '批量自定义JSON导出', 'export.selectFolder': '正在请求文件夹权限...', 'export.complete': '导出完成!', // Tree view 相关 'tree.preview': '对话树预览', 'tree.loading': '正在加载对话树...', 'tree.empty': '这是一个空对话', 'tree.emptyForBranching': ',无法选择节点', // Common 相关 'common.confirm': '确定', 'common.cancel': '取消', 'common.close': '关闭', 'common.save': '保存', 'common.loading': '加载中...', 'common.error': '错误', 'common.success': '成功', 'common.failed': '失败', // Sorting & Filtering 'sort.updatedDesc': '时间降序', 'sort.updatedAsc': '时间升序', 'sort.nameAsc': '名称 A-Z', 'sort.nameDesc': '名称 Z-A', 'filter.all': '显示全部', 'filter.starred': '仅显示收藏', 'filter.unstarred': '隐藏收藏', 'filter.asciiOnly': '仅显示纯ASCII标题', 'filter.nonAscii': '不显示纯ASCII标题', // Button tooltips 'tooltip.managerButton': 'Tips: Ctrl + M 可以隐藏此按钮', 'tooltip.navigatorButton': '从对话历史的任意节点延续&导航至任意节点', 'tooltip.linearNavButton': '线性导航', 'tooltip.pdfButton': '打开PDF上传设置', 'tooltip.pdfHelp': '此功能为普通账户设计,可强制使用高级解析路径。Pro/Team账户原生支持,此开关对其无效。', 'tooltip.settingsButton': '设置', 'tooltip.githubLink': '查看 GitHub 仓库', 'tooltip.studioLink': '了解下一个项目: claude-dialog-tree-studio', 'pdf.forceModeText': 'Force PDF Deep Analysis', // Toolbar labels 'toolbar.sort': '排序:', 'toolbar.filter': '筛选:', 'toolbar.searchPlaceholder': '搜索标题...', // Batch operations detailed settings 'batchOps.starUnstar': '批量收藏/取消收藏', 'batchOps.refreshAfterStar': '操作后从服务器刷新列表 (否则仅更新当前视图)', 'batchOps.batchDelete': '批量删除', 'batchOps.refreshAfterDelete': '操作后从服务器刷新列表 (否则仅更新当前视图)', 'batchOps.autoRename': '批量自动重命名', 'batchOps.titleLanguage': '标题语言:', 'batchOps.titleLanguagePlaceholder': '例如:中文, English, 日本語', 'batchOps.maxRounds': '使用对话轮数 (最多):', 'batchOps.refreshAfterRename': '操作后从服务器刷新列表 (否则仅更新当前视图)', // Export settings 'exportSettings.customOptions': '自定义导出选项', 'exportSettings.batchCustomOptions': '批量自定义导出选项', 'exportSettings.exportNow': '立即导出', 'exportSettings.batchExportNow': '开始批量导出', 'exportSettings.basicInfo': '基础信息', 'exportSettings.messageStructure': '消息结构', 'exportSettings.timestampInfo': '时间戳信息', 'exportSettings.coreContent': '核心内容', 'exportSettings.advancedContent': '高级内容', 'exportSettings.keepMetadata': '保留会话元数据', 'exportSettings.title': '标题 (name)', 'exportSettings.summary': '摘要 (summary)', 'exportSettings.sessionTimestamp': '会话创建/更新时间', 'exportSettings.sessionSettings': '会话设置 (settings)', 'exportSettings.sender': '发送者 (sender)', 'exportSettings.messageUuids': '消息/父级UUID (建议保留)', 'exportSettings.otherMeta': '其他元数据 (index, stop_reason等)', 'exportSettings.messageTimestamp': '消息节点时间戳 (created_at/updated_at)', 'exportSettings.contentTimestamp': '内容块流式时间戳 (start/stop)', 'exportSettings.attachmentTimestamp': '附件创建时间戳', 'exportSettings.textContent': '文本内容 (text块)', 'exportSettings.attachmentInfo': '附件信息:', 'exportSettings.attachmentFull': '完整信息 (含提取文本)', 'exportSettings.attachmentMetaOnly': '仅元数据 (文件名,大小等)', 'exportSettings.attachmentNone': '不保留附件', 'exportSettings.thinkingProcess': "'思考'过程 (thinking块)", 'exportSettings.toolRecords': '保留工具使用记录', 'exportSettings.webSearch': '网页搜索 (web_search)', 'exportSettings.codeAnalysis': '代码分析 (repl)', 'exportSettings.artifactCreation': '工件创建 (artifacts)', 'exportSettings.otherTools': '其他未知工具', 'exportSettings.successfulOnly': '仅保留成功的工具调用', // Export status messages 'exportStatus.customComplete': '自定义导出完成!', 'exportStatus.customFailed': '自定义导出失败', 'exportStatus.batchPreparing': '准备批量自定义导出', 'exportStatus.batchComplete': '批量自定义导出完成', 'exportStatus.sessions': '个会话', // Action button tooltips 'action.manualRename': '手动重命名', 'action.previewTree': '预览对话树', 'action.originalExport': '原始JSON导出', 'action.customExport': '自定义JSON导出', // Status messages 'status.savingTitle': '正在保存新标题...', 'status.saveSuccess': '保存成功!', 'status.saveFailed': '保存失败', 'status.loadedSessions': '已加载', 'status.loadSessionsFailed': '加载会话失败', 'status.loadFailed': '加载失败', // Error messages 'error.cannotLoadTree': '无法加载对话树', // Tree view related 'treeView.prefix': '对话树: ', 'treeView.untitled': '无标题', 'treeView.loading': '加载中...', 'treeView.loadFailed': '无法加载对话树', // Additional status and error messages 'error.invalidTitle': '生成了无效标题。', 'error.loadSessionsFailed': '加载会话失败', 'error.selectSessions': '请选择要执行"{0}"的会话。', 'error.selectExportSessions': '请选择要导出的会话。', 'error.browserNotSupported': '您的浏览器不支持 File System Access API。', 'status.refreshingFromServer': ' 正在从服务器刷新列表...', 'status.preparingExport': '准备导出...', 'status.exporting': '正在导出...', 'status.ready': '准备就绪。', 'navigator.loadingHistory': '正在加载对话历史...', 'navigator.notInChat': '不在具体聊天内,无法操作节点。', 'attachment.title': 'PDF深度解析暂存区', 'attachment.removeFile': '移除文件', 'attachment.clickPreview': '点击预览', 'attachment.openInNewTab': '点击在新标签页打开', 'navigator.clickToNavigate': '点击导航到此节点', 'navigator.clickToContinue': '点击从此节点继续对话', 'navigator.nextMessageFrom': '下条消息将从指定节点开始。', 'batchOps.confirmDelete': '确定永久删除 {0} 个会话吗?', 'status.batchProcessing': '正在批量{0} {1} 个会话...', 'status.batchItemProcessing': '正在{0} {1}/{2}...', 'status.batchItemFailed': '第{0}个失败', 'status.batchOperationComplete': '操作完成。成功{0} {1}/{2} 个会话。', 'status.batchOperationFailed': '批量{0}失败', 'status.batchExportPreparing': '准备批量导出 {0} 个会话...', 'status.batchExportFailed': '批量导出失败', 'status.exportFailed': '导出失败', 'status.checkingFile': '检查文件 {0} 出错', 'status.writingFile': '正在写入 {0}...', 'status.convertingData': '正在根据设置转换数据...', // Export related messages 'export.foundAttachments': '发现 {0} 个附件,开始下载...', 'export.cannotGetOrgInfo': '无法获取组织信息以下载附件。', 'export.skipExistingFile': '({0}/{1}) 跳过 (文件已存在): {2}', 'export.downloading': '({0}/{1}) 正在下载: {2}', 'export.noDownloadUrl': '找不到附件的下载链接。', 'export.processAttachmentFailed': '处理附件 {0} 失败', 'export.requestingFolder': '正在请求文件夹权限...', 'export.userCancelled': '用户取消了文件夹选择。', 'export.orgInfoRequired': '缺少导出所需组织信息。', 'export.creatingDirectory': '正在创建目录...', 'export.originalComplete': '原始导出完成!', 'export.originalFailed': '原始导出失败', 'export.customFailed': '自定义导出失败', 'export.batchComplete': '批量导出完成: {0}/{1} 个会话成功导出。', 'export.exportFailed': '导出失败 ({0}/{1}): {2}', 'export.exportingProgress': '({0}/{1}) 正在导出: {2}', 'export.sessionFailed': '导出会话 {0} 失败', // API error messages 'api.orgRequestFailed': '组织API请求失败: {0}', 'api.orgInfoNotFound': '在API响应中未找到组织信息。', 'api.getSessionsFailed': '获取会话列表失败: {0}', 'api.getHistoryFailed': '获取历史记录失败: {0}', 'api.deleteRequestFailed': '删除API请求失败: {0}', 'api.titleGenerationFailed': '标题生成API请求失败。', 'api.updateSessionFailed': '更新会话失败: {0}', 'api.fileDownloadFailed': '文件下载失败: {0} at {1}', // Tree view and content messages 'tree.attachmentOrToolOnly': '[仅包含附件或工具使用]', 'tree.attachments': '附件', 'tree.dirtyData': '脏数据', 'error.checkingFile': '检查文件 {0} 时发生意外错误', 'error.noValidTextContent': '在指定轮次内未找到有效文本内容。', 'error.insufficientRounds': '对话轮次不足(可能为空对话),跳过重命名。', 'error.cannotGetConvoData': '无法获取对话数据', // Operation names 'operation.rename': '重命名', 'operation.delete': '删除', 'operation.star': '收藏', 'operation.unstar': '取消收藏' }, en: { // Manager related 'manager.title': 'Manager', 'manager.refresh': 'Refresh List', 'manager.selectAll': 'Select All', 'manager.selectNone': 'Select None', 'manager.selectInvert': 'Invert Selection', 'manager.batchStar': 'Batch Star', 'manager.batchUnstar': 'Batch Unstar', 'manager.batchRename': 'Batch Auto Rename', 'manager.batchDelete': 'Batch Delete', 'manager.loading': 'Loading conversations...', 'manager.noResults': 'No conversations match the criteria.', 'manager.ready': 'Ready.', 'manager.refreshButtonTip': 'Click refresh button ( ) to load conversation list.', // Settings related 'settings.title': 'Manager Settings', 'settings.theme': 'Appearance Settings', 'settings.themeMode': 'Script Theme:', 'settings.themeAuto': 'Follow Website', 'settings.themeLight': 'Lock Light', 'settings.themeDark': 'Lock Dark', 'settings.batchOps': 'Batch Operations Settings', 'settings.exportDefaults': 'Custom Export Default Settings', 'settings.save': 'Save Settings', 'settings.saved': 'Settings saved!', 'settings.backToMain': 'Back to Main Panel', 'settings.language': 'Language Settings', 'settings.interfaceLanguage': 'Interface Language:', // Navigator related 'navigator.title': 'Dialog Node Continuation & Navigator', 'navigator.branchMode': 'Branch Mode', 'navigator.navigateMode': 'Navigate Mode', 'navigator.branchSelected': 'Branch point selected', 'navigator.loading': 'Loading conversation history...', 'navigator.branchFromRoot': 'Start from root node (create a new main branch)', // Linear Navigator related 'linear.title': 'Linear Navigation', 'linear.refresh': 'Refresh Dialog List', 'linear.close': 'Close Linear Navigation', 'linear.top': 'Go to Top', 'linear.bottom': 'Go to Bottom', 'linear.prev': 'Previous', 'linear.next': 'Next', 'linear.empty': 'No linear dialogs', // Attachment related 'attachment.title': 'PDF Deep Analysis Staging Area', 'attachment.forceMode': 'Force PDF Deep Analysis', 'attachment.close': 'Close and clear all staged files', // Export related 'export.original': 'Original JSON Export', 'export.custom': 'Custom JSON Export', 'export.batchOriginal': 'Batch Original JSON Export', 'export.batchCustom': 'Batch Custom JSON Export', 'export.selectFolder': 'Requesting folder permission...', 'export.complete': 'Export completed!', // Tree view related 'tree.preview': 'Dialog Tree Preview', 'tree.loading': 'Loading dialog tree...', 'tree.empty': 'This is an empty conversation', 'tree.emptyForBranching': ', cannot select nodes', // Common related 'common.confirm': 'Confirm', 'common.cancel': 'Cancel', 'common.close': 'Close', 'common.save': 'Save', 'common.loading': 'Loading...', 'common.error': 'Error', 'common.success': 'Success', 'common.failed': 'Failed', // Sorting & Filtering 'sort.updatedDesc': 'Time Desc', 'sort.updatedAsc': 'Time Asc', 'sort.nameAsc': 'Name A-Z', 'sort.nameDesc': 'Name Z-A', 'filter.all': 'Show All', 'filter.starred': 'Show Starred Only', 'filter.unstarred': 'Hide Starred', 'filter.asciiOnly': 'ASCII Titles Only', 'filter.nonAscii': 'Non-ASCII Titles Only', // Button tooltips 'tooltip.managerButton': 'Tips: Ctrl + M to hide this button', 'tooltip.navigatorButton': 'Branch from any message node & navigate to any node', 'tooltip.linearNavButton': 'Linear Navigation', 'tooltip.pdfButton': 'Open PDF upload settings', 'tooltip.pdfHelp': 'This feature is designed for regular accounts to force advanced parsing. Pro/Team accounts natively support this, so this toggle has no effect.', 'tooltip.settingsButton': 'Settings', 'tooltip.githubLink': 'View GitHub Repository', 'tooltip.studioLink': 'Learn about next project: claude-dialog-tree-studio', 'pdf.forceModeText': 'Force PDF Deep Analysis', // Toolbar labels 'toolbar.sort': 'Sort:', 'toolbar.filter': 'Filter:', 'toolbar.searchPlaceholder': 'Search titles...', // Batch operations detailed settings 'batchOps.starUnstar': 'Batch Star/Unstar', 'batchOps.refreshAfterStar': 'Refresh list from server after operation (otherwise only update current view)', 'batchOps.batchDelete': 'Batch Delete', 'batchOps.refreshAfterDelete': 'Refresh list from server after operation (otherwise only update current view)', 'batchOps.autoRename': 'Batch Auto Rename', 'batchOps.titleLanguage': 'Title Language:', 'batchOps.titleLanguagePlaceholder': 'e.g.: 中文, English, 日本語', 'batchOps.maxRounds': 'Max Conversation Rounds:', 'batchOps.refreshAfterRename': 'Refresh list from server after operation (otherwise only update current view)', // Export settings 'exportSettings.customOptions': 'Custom Export Options', 'exportSettings.batchCustomOptions': 'Batch Custom Export Options', 'exportSettings.exportNow': 'Export Now', 'exportSettings.batchExportNow': 'Start Batch Export', 'exportSettings.basicInfo': 'Basic Information', 'exportSettings.messageStructure': 'Message Structure', 'exportSettings.timestampInfo': 'Timestamp Information', 'exportSettings.coreContent': 'Core Content', 'exportSettings.advancedContent': 'Advanced Content', 'exportSettings.keepMetadata': 'Keep conversation metadata', 'exportSettings.title': 'Title (name)', 'exportSettings.summary': 'Summary (summary)', 'exportSettings.sessionTimestamp': 'Session creation/update time', 'exportSettings.sessionSettings': 'Session settings (settings)', 'exportSettings.sender': 'Sender (sender)', 'exportSettings.messageUuids': 'Message/Parent UUIDs (recommended to keep)', 'exportSettings.otherMeta': 'Other metadata (index, stop_reason, etc.)', 'exportSettings.messageTimestamp': 'Message node timestamps (created_at/updated_at)', 'exportSettings.contentTimestamp': 'Content block streaming timestamps (start/stop)', 'exportSettings.attachmentTimestamp': 'Attachment creation timestamps', 'exportSettings.textContent': 'Text content (text blocks)', 'exportSettings.attachmentInfo': 'Attachment Information:', 'exportSettings.attachmentFull': 'Full information (including extracted text)', 'exportSettings.attachmentMetaOnly': 'Metadata only (filename, size, etc.)', 'exportSettings.attachmentNone': 'No attachments', 'exportSettings.thinkingProcess': "'Thinking' process (thinking blocks)", 'exportSettings.toolRecords': 'Keep tool usage records', 'exportSettings.webSearch': 'Web search (web_search)', 'exportSettings.codeAnalysis': 'Code analysis (repl)', 'exportSettings.artifactCreation': 'Artifact creation (artifacts)', 'exportSettings.otherTools': 'Other unknown tools', 'exportSettings.successfulOnly': 'Keep only successful tool calls', // Export status messages 'exportStatus.customComplete': 'Custom export completed!', 'exportStatus.customFailed': 'Custom export failed', 'exportStatus.batchPreparing': 'Preparing batch custom export', 'exportStatus.batchComplete': 'Batch custom export completed', 'exportStatus.sessions': 'sessions', // Action button tooltips 'action.manualRename': 'Manual Rename', 'action.previewTree': 'Preview Dialog Tree', 'action.originalExport': 'Original JSON Export', 'action.customExport': 'Custom JSON Export', // Status messages 'status.savingTitle': 'Saving new title...', 'status.saveSuccess': 'Saved successfully!', 'status.saveFailed': 'Save failed', 'status.loadedSessions': 'Loaded', 'status.loadSessionsFailed': 'Failed to load conversations', 'status.loadFailed': 'Load failed', // Error messages 'error.cannotLoadTree': 'Cannot load conversation tree', // Tree view related 'treeView.prefix': 'Dialog Tree: ', 'treeView.untitled': 'Untitled', 'treeView.loading': 'Loading...', 'treeView.loadFailed': 'Failed to load dialog tree', // Additional status and error messages 'error.invalidTitle': 'Generated invalid title.', 'error.loadSessionsFailed': 'Failed to load sessions', 'error.selectSessions': 'Please select sessions to execute "{0}".', 'error.selectExportSessions': 'Please select sessions to export.', 'error.browserNotSupported': 'Your browser does not support File System Access API.', 'status.refreshingFromServer': ' Refreshing from server...', 'status.preparingExport': 'Preparing export...', 'status.exporting': 'Exporting...', 'status.ready': 'Ready.', 'navigator.loadingHistory': 'Loading conversation history...', 'navigator.notInChat': 'Not in a specific chat, cannot operate on nodes.', 'attachment.title': 'PDF Deep Analysis Staging Area', 'attachment.removeFile': 'Remove file', 'attachment.clickPreview': 'Click to preview', 'attachment.openInNewTab': 'Click to open in new tab', 'navigator.clickToNavigate': 'Click to navigate to this node', 'navigator.clickToContinue': 'Click to continue from this node', 'navigator.nextMessageFrom': 'Next message will start from the specified node.', 'batchOps.confirmDelete': 'Are you sure to permanently delete {0} conversations?', 'status.batchProcessing': 'Batch {0} {1} conversations...', 'status.batchItemProcessing': '{0} {1}/{2}...', 'status.batchItemFailed': 'Item {0} failed', 'status.batchOperationComplete': 'Operation completed. Successfully {0} {1}/{2} conversations.', 'status.batchOperationFailed': 'Batch {0} failed', 'status.batchExportPreparing': 'Preparing batch export for {0} conversations...', 'status.batchExportFailed': 'Batch export failed', 'status.exportFailed': 'Export failed', 'status.checkingFile': 'Error checking file {0}', 'status.writingFile': 'Writing {0}...', 'status.convertingData': 'Converting data according to settings...', // Export related messages 'export.foundAttachments': 'Found {0} attachments, starting download...', 'export.cannotGetOrgInfo': 'Cannot get organization info for downloading attachments.', 'export.skipExistingFile': '({0}/{1}) Skip (file exists): {2}', 'export.downloading': '({0}/{1}) Downloading: {2}', 'export.noDownloadUrl': 'Cannot find download link for attachment.', 'export.processAttachmentFailed': 'Failed to process attachment {0}', 'export.requestingFolder': 'Requesting folder permissions...', 'export.userCancelled': 'User cancelled folder selection.', 'export.orgInfoRequired': 'Organization info required for export.', 'export.creatingDirectory': 'Creating directory...', 'export.originalComplete': 'Original export completed!', 'export.originalFailed': 'Original export failed', 'export.customFailed': 'Custom export failed', 'export.batchComplete': 'Batch export completed: {0}/{1} conversations exported successfully.', 'export.exportFailed': 'Export failed ({0}/{1}): {2}', 'export.exportingProgress': '({0}/{1}) Exporting: {2}', 'export.sessionFailed': 'Failed to export session {0}', // API error messages 'api.orgRequestFailed': 'Organization API request failed: {0}', 'api.orgInfoNotFound': 'Organization info not found in API response.', 'api.getSessionsFailed': 'Failed to get sessions: {0}', 'api.getHistoryFailed': 'Failed to get history: {0}', 'api.deleteRequestFailed': 'Delete API request failed: {0}', 'api.titleGenerationFailed': 'Title generation API request failed.', 'api.updateSessionFailed': 'Failed to update session: {0}', 'api.fileDownloadFailed': 'File download failed: {0} at {1}', // Tree view and content messages 'tree.attachmentOrToolOnly': '[Contains only attachments or tool usage]', 'tree.attachments': 'Attachments', 'tree.dirtyData': 'Dirty Data', 'error.checkingFile': 'Unexpected error checking file {0}', 'error.noValidTextContent': 'No valid text content found within specified rounds.', 'error.insufficientRounds': 'Insufficient conversation rounds (possibly empty conversation), skipping rename.', 'error.cannotGetConvoData': 'Cannot get conversation data', // Operation names 'operation.rename': 'rename', 'operation.delete': 'delete', 'operation.star': 'star', 'operation.unstar': 'unstar' } } }; // 翻译函数 function t(key, fallback = key, ...params) { const lang = I18N_CONFIG.currentLang; const translations = I18N_CONFIG.translations[lang]; let text = translations && translations[key] ? translations[key] : (fallback || key); // 支持参数替换 {0}, {1}, {2}... if (params.length > 0) { for (let i = 0; i < params.length; i++) { text = text.replace(new RegExp(`\\{${i}\\}`, 'g'), params[i]); } } return text; } // ========================================================================= // 1. 全局配置 // ========================================================================= 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"], SpecialContent: [".doc", ".pptx", ".zip"], PdfHandler: [".pdf"], OutOfContentFileHandler: [".csv", ".xls", ".xlsx", ".xlsb", ".xlm", ".xlsm", ".xlt", ".xltm", ".xltx", ".ods", ".zip"], ContentExtractorHandler: [".docx", ".rtf", ".epub", ".odt", ".odp"], 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', maxPreviewLength: 16, refreshInterval: 150, topMargin: 200, STORAGE_KEY: 'cpm-ln-panel-state' }; // ========================================================================= // 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: t('settings.theme'), render() { return `
`; }, 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: t('settings.batchOps'), render() { return `

${t('batchOps.starUnstar')}

${t('batchOps.batchDelete')}

${t('batchOps.autoRename')}

`; }, 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: t('settings.exportDefaults'), render() { return ManagerUI.createExportSettingsHTML(true); }, load(container) { ManagerUI.loadExportSettings(container); }, save(container) { ManagerUI.saveExportSettings(container); }, addEventListeners(container) { ManagerUI.setupSubOptionDisabling(container); } }; // --- 2.4 语言设置模块 --- const LanguageSettingsModule = { id: 'language', title: t('settings.language'), render() { return `
`; }, load(container) { const langSelect = container.querySelector('#cpm-language-select'); if (langSelect) { langSelect.value = I18N_CONFIG.currentLang; } }, save(container) { const langSelect = container.querySelector('#cpm-language-select'); if (langSelect) { const newLang = langSelect.value; if (newLang !== I18N_CONFIG.currentLang) { I18N_CONFIG.currentLang = newLang; GM_setValue('language', newLang); // 重新加载界面 setTimeout(() => { location.reload(); }, 500); } } } }; // --- 2.5 注册所有设置模块 --- SettingsRegistry.register(LanguageSettingsModule); SettingsRegistry.register(ThemeSettingsModule); SettingsRegistry.register(BatchOpsSettingsModule); SettingsRegistry.register(ExportSettingsModule); // ========================================================================= // 3. 主题管理器 (共享) // ========================================================================= const ThemeManager = { init() { this.applyCurrentTheme(); this.observer = new MutationObserver(() => this.applyCurrentTheme()); this.observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-mode'] }); console.log(LOG_PREFIX, "主题管理器已初始化并开始监听。"); }, cleanup() { if (this.observer) { this.observer.disconnect(); this.observer = null; } }, 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. 存储管理器和文本处理工具 // ========================================================================= const StorageManager = { getPanelState() { try { const state = localStorage.getItem(Config.STORAGE_KEY); return state === 'true'; } catch (e) { return false; } }, setPanelState(isOpen) { try { localStorage.setItem(Config.STORAGE_KEY, String(isOpen)); } catch (e) { // 忽略存储错误 } } }; const TextUtils = { getPreview(element, maxLength = Config.maxPreviewLength) { if (!element) return ''; const text = (element.innerText || element.textContent || '') .replace(/\s+/g, ' ').trim(); if (!text) return ''; let width = 0, result = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; const charWidth = /[\u4e00-\u9fa5]/.test(char) ? 2 : 1; if (width + charWidth > maxLength) { result += '…'; break; } result += char; width += charWidth; } return result || text.slice(0, maxLength); } }; // ========================================================================= // 5. API 层 (共享) // ========================================================================= const ClaudeAPI = { orgUuid: null, orgInfo: null, conversationTree: null, currentLinearBranch: null, currentConversationUuid: null, isInitialized: false, async getOrganizationInfo() { if (this.orgInfo) return this.orgInfo; try { const response = await fetch('/api/organizations'); if (!response.ok) throw new Error(t('api.orgRequestFailed', 'api.orgRequestFailed', 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(t('api.orgInfoNotFound')); } 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(t('api.getSessionsFailed', 'api.getSessionsFailed', 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(t('api.getHistoryFailed', 'api.getHistoryFailed', response.status)); const data = await response.json(); // 标记脏数据:标记没有 Claude 回复的孤儿用户节点 (在副本上操作) const processedMessages = this.markDirtyMessages(data.chat_messages); this.conversationTree = this.buildConversationTree(processedMessages); this.updateCurrentLinearBranch(); return data; // 返回原始数据,保持不变 }, markDirtyMessages(messages) { // 创建消息的深度副本,避免修改原始数据 const messagesCopy = messages.map(msg => JSON.parse(JSON.stringify(msg))); // 按 index 排序消息以确保正确的时间顺序 const sortedMessages = [...messagesCopy].sort((a, b) => a.index - b.index); let dirtyCount = 0; console.log(`${LOG_PREFIX} 开始标记脏数据,原始消息数量: ${messages.length}`); for (let i = 0; i < sortedMessages.length; i++) { const currentMessage = sortedMessages[i]; // 如果是用户消息,检查下一个消息是否是 Claude 的回复 if (currentMessage.sender === 'human') { const nextMessage = sortedMessages[i + 1]; // 如果没有下一个消息,或者下一个消息也是用户消息,说明是孤儿用户节点 if (!nextMessage || nextMessage.sender === 'human') { console.log(`${LOG_PREFIX} 发现孤儿用户节点: ${currentMessage.uuid.slice(-8)}, index: ${currentMessage.index}, 内容: "${currentMessage.content?.[0]?.text?.slice(0, 50) || '空内容'}..."`); // 在副本上标记为脏数据 currentMessage._isDirtyData = true; dirtyCount++; } } } if (dirtyCount > 0) { console.log(`${LOG_PREFIX} 标记完成,标记了 ${dirtyCount} 个孤儿用户节点为脏数据,总消息数量: ${messagesCopy.length}`); } return messagesCopy; // 返回包含脏数据标记的副本消息列表 }, buildConversationTree(messages) { // 创建消息的深度副本,避免修改原始数据 const nodesCopy = {}; messages.forEach(msg => { nodesCopy[msg.uuid] = JSON.parse(JSON.stringify(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(nodesCopy[a].created_at) - new Date(nodesCopy[b].created_at)); } function assignIdsRecursive(nodeUuid, prefix) { if (!nodesCopy[nodeUuid]) return; const node = nodesCopy[nodeUuid]; // 在副本上添加 tree_id node.tree_id = prefix; const children = childrenMap[nodeUuid] || []; let normalIndex = 0; let dirtyCount = 1; children.forEach((childUuid) => { const childNode = nodesCopy[childUuid]; if (!childNode) return; // 检测脏数据:标记了 _isDirtyData 的节点 const isDirtyData = childNode._isDirtyData; if (isDirtyData) { assignIdsRecursive(childUuid, `${prefix}-F${dirtyCount}`); dirtyCount++; } else { assignIdsRecursive(childUuid, `${prefix}-${normalIndex}`); normalIndex++; } }); } const rootNodes = childrenMap[Config.INITIAL_PARENT_UUID] || []; rootNodes.forEach((rootUuid, index) => { assignIdsRecursive(rootUuid, `root-${index}`); }); return { nodes: nodesCopy, childrenMap, rootNodes }; }, 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(t('api.deleteRequestFailed', 'api.deleteRequestFailed', 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(t('api.titleGenerationFailed')); const { title } = await response.json(); if (!title || title.toLowerCase().includes('untitled')) throw new Error(t('error.invalidTitle')); 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(t('api.updateSessionFailed', 'api.updateSessionFailed', response.statusText)); }, async downloadFile(url) { const response = await fetch(url); if (!response.ok) throw new Error(t('api.fileDownloadFailed', 'api.fileDownloadFailed', response.status, url)); return response.blob(); }, async updateCurrentLinearBranch() { // 分析当前前端显示的线性分支 if (!this.conversationTree) { // 尝试自动初始化对话树 await this.tryInitializeConversationTree(); if (!this.conversationTree) { this.currentLinearBranch = []; return; } } const turns = this.findCurrentTurns(); // 构建线性分支:前端DOM显示的必定是完整的父子串行关系 const branch = this.buildLinearBranchFromDOM(turns); this.currentLinearBranch = branch; return turns; // 返回turns避免重复调用 }, async tryInitializeConversationTree() { // 智能初始化对话树 - 避免重复请求 try { const currentUrl = window.location.href; const pathParts = new URL(currentUrl).pathname.split('/'); const conversationUuid = (pathParts[1] === 'chat' && pathParts[2]) ? pathParts[2] : null; if (!conversationUuid || conversationUuid === 'new') { return false; } // 检查是否已经为当前对话初始化过 if (this.isInitialized && this.currentConversationUuid === conversationUuid) { return true; } // 初始化新的对话树 await this.getConversationHistory(conversationUuid); this.currentConversationUuid = conversationUuid; this.isInitialized = true; return true; } catch (error) { console.warn(`对话树初始化失败:`, error); this.isInitialized = false; } return false; }, // 检测对话切换,重置初始化状态 checkConversationChange() { const currentUrl = window.location.href; const pathParts = new URL(currentUrl).pathname.split('/'); const conversationUuid = (pathParts[1] === 'chat' && pathParts[2]) ? pathParts[2] : null; if (conversationUuid !== this.currentConversationUuid) { console.log(`检测到对话切换: ${this.currentConversationUuid?.slice(-8) || 'none'} → ${conversationUuid?.slice(-8) || 'none'}`); this.isInitialized = false; this.conversationTree = null; this.currentLinearBranch = null; this.currentConversationUuid = conversationUuid; } }, buildLinearBranchFromDOM(turns) { // 基于DOM的串行父子关系构建分支 const branch = []; let expectedParentUuid = Config.INITIAL_PARENT_UUID; // 根节点UUID(虚拟,不在前端显示) // 预先提取所有兄弟信息,避免重复调用 const turnsWithSiblingInfo = turns.map(turn => ({ turn, siblingInfo: this.extractSiblingInfo(turn) })); // 由于根节点不在前端显示,第一个DOM回合对应根节点的直接子节点 for (let i = 0; i < turnsWithSiblingInfo.length; i++) { const { turn, siblingInfo } = turnsWithSiblingInfo[i]; const nodeUuid = this.findNodeByPositionWithCachedInfo(turn, expectedParentUuid, siblingInfo); if (nodeUuid) { const node = { ...this.conversationTree.nodes[nodeUuid], uuid: nodeUuid }; // 检查是否为脏数据,如果是则跳过(正常情况下不应该发生,因为脏数据不应该出现在DOM中) if (!node._isDirtyData) { branch.push(node); expectedParentUuid = nodeUuid; // 下一个节点的父节点就是当前节点 } else { console.warn(`DOM回合 ${i + 1} 匹配到脏数据节点,跳过: ${nodeUuid.slice(-8)}`); } } else { console.warn(`DOM回合 ${i + 1} 无法匹配节点 (期望父: ${expectedParentUuid.slice(-8)})`); break; // 中断构建,因为父子关系链断裂 } } return branch; }, findNodeByPosition(turnElement, expectedParentUuid) { // 基于位置信息在对话树中找到精确的节点 const siblingInfo = this.extractSiblingInfo(turnElement); return this.findNodeByPositionWithCachedInfo(turnElement, expectedParentUuid, siblingInfo); }, findNodeByPositionWithCachedInfo(turnElement, expectedParentUuid, siblingInfo) { // 基于位置信息和缓存的兄弟信息在对话树中找到精确的节点 const isUser = !!turnElement.querySelector('[data-testid="user-message"]'); const expectedSender = isUser ? 'human' : 'assistant'; const { nodes, childrenMap } = this.conversationTree; // 获取期望父节点的所有子节点,排除脏数据 const siblings = childrenMap[expectedParentUuid] || []; const sameTypeSiblings = siblings.filter(uuid => nodes[uuid] && nodes[uuid].sender === expectedSender && !nodes[uuid]._isDirtyData ); if (siblingInfo) { // 有兄弟信息时,使用精确位置匹配 if (sameTypeSiblings.length === siblingInfo.totalSiblings) { const targetIndex = siblingInfo.currentIndex; if (targetIndex >= 0 && targetIndex < sameTypeSiblings.length) { return sameTypeSiblings[targetIndex]; } } else { console.warn(`兄弟数量不匹配: DOM显示${siblingInfo.totalSiblings}个, 树中有${sameTypeSiblings.length}个`); } } else { // 没有兄弟信息时的处理逻辑 if (sameTypeSiblings.length === 1) { return sameTypeSiblings[0]; } else if (sameTypeSiblings.length > 1) { // 选择时间最早的节点(通常是主分支) const sortedSiblings = sameTypeSiblings.sort((a, b) => new Date(nodes[a].created_at) - new Date(nodes[b].created_at) ); return sortedSiblings[0]; } } return null; }, findCurrentTurns() { // 基于实际DOM结构查找对话回合 - 只使用最精确的选择器 const elements = document.querySelectorAll('div[data-test-render-count]'); const validElements = Array.from(elements).filter(el => { // 检查是否包含用户消息或Claude响应内容 const hasUserMessage = !!el.querySelector('[data-testid="user-message"]'); const hasClaudeResponse = !!el.querySelector('.font-claude-response'); // 必须是用户消息或Claude响应之一 return hasUserMessage || hasClaudeResponse; }); return validElements; }, extractSiblingInfo(turnElement) { // 查找关键定位元素:a / b // 其中 b 代表包括自己在内总共有多少个兄弟节点,a 代表自己处于兄弟节点的第几个(1基索引) // 精确的类名匹配 let siblingSpan = turnElement.querySelector('span.self-center.shrink-0.select-none.font-small.text-text-300'); if (siblingSpan) { const text = siblingSpan.textContent?.trim(); const match = text.match(/(\d+)\s*\/\s*(\d+)/); if (match) { const currentPosition = parseInt(match[1]); // 1基索引位置 const totalSiblings = parseInt(match[2]); // 包括自己在内的总数 return { currentIndex: currentPosition - 1, // 转换为0基索引用于数组操作 totalSiblings: totalSiblings }; } } return null; }, extractNodeText(node) { if (node.content && Array.isArray(node.content)) { // 根据真实数据格式提取文本 for (const contentBlock of node.content) { if (contentBlock.type === 'text' && contentBlock.text) { return contentBlock.text; } } } return node.text || ''; }, // 检查目标节点是否在当前线性分支中 isNodeInCurrentBranch(nodeUuid) { if (!this.currentLinearBranch) return false; return this.currentLinearBranch.some(node => node && node.uuid === nodeUuid); } }; // ========================================================================= // 5. 分支切换核心功能 // ========================================================================= const BranchSwitcher = { // 基础工具函数 wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, // 切换到阶段性目标节点 async switchToTargetStageNode(targetNodeUuid) { console.log(`${LOG_PREFIX} 尝试切换到阶段性目标节点: ${targetNodeUuid.slice(-8)}`); // 1. 判断阶段性目标节点是否在前端 if (ClaudeAPI.isNodeInCurrentBranch(targetNodeUuid)) { console.log(`${LOG_PREFIX} 目标节点已在当前前端显示`); return true; } // 2. 判断阶段性目标节点的父节点是否在前端 const { nodes } = ClaudeAPI.conversationTree; const targetNode = nodes[targetNodeUuid]; if (!targetNode) { console.error(`${LOG_PREFIX} 找不到目标节点: ${targetNodeUuid.slice(-8)}`); return false; } // 检查目标节点是否为脏数据 if (targetNode._isDirtyData) { console.error(`${LOG_PREFIX} 目标节点是脏数据,无法切换: ${targetNodeUuid.slice(-8)}`); return false; } const parentUuid = targetNode.parent_message_uuid || Config.INITIAL_PARENT_UUID; // 如果父节点是根节点,检查根节点是否等效在前端(即第一个节点的父节点) let isParentInFrontend = false; if (parentUuid === Config.INITIAL_PARENT_UUID) { // 根节点场景:只要当前分支有节点,根节点就等效在前端 isParentInFrontend = ClaudeAPI.currentLinearBranch && ClaudeAPI.currentLinearBranch.length > 0; } else { isParentInFrontend = ClaudeAPI.isNodeInCurrentBranch(parentUuid); } if (!isParentInFrontend) { console.log(`${LOG_PREFIX} 目标节点的父节点不在前端显示: ${parentUuid.slice(-8)}`); return false; } // 3. 计算要执行的操作 const { childrenMap } = ClaudeAPI.conversationTree; const siblings = childrenMap[parentUuid] || []; const sameTypeSiblings = siblings.filter(uuid => nodes[uuid] && nodes[uuid].sender === targetNode.sender && !nodes[uuid]._isDirtyData ); // 3.1 计算阶段性目标节点在其父节点的子节点中的位置index const targetIndex = sameTypeSiblings.indexOf(targetNodeUuid); if (targetIndex === -1) { console.error(`${LOG_PREFIX} 在兄弟节点中找不到目标节点`); return false; } // 3.2 计算当前前端显示的兄弟节点位置 let currentIndex = -1; if (parentUuid === Config.INITIAL_PARENT_UUID) { // 根节点场景:查找第一个同类型节点 if (ClaudeAPI.currentLinearBranch && ClaudeAPI.currentLinearBranch.length > 0) { const firstNodeOfSameType = ClaudeAPI.currentLinearBranch.find(node => node && node.sender === targetNode.sender ); if (firstNodeOfSameType) { currentIndex = sameTypeSiblings.indexOf(firstNodeOfSameType.uuid); } } } else { // 非根节点场景:查找父节点后的第一个同类型节点 const parentIndexInBranch = ClaudeAPI.currentLinearBranch.findIndex(node => node && node.uuid === parentUuid ); if (parentIndexInBranch !== -1) { for (let i = parentIndexInBranch + 1; i < ClaudeAPI.currentLinearBranch.length; i++) { const node = ClaudeAPI.currentLinearBranch[i]; if (node && node.sender === targetNode.sender) { currentIndex = sameTypeSiblings.indexOf(node.uuid); break; } } } } if (currentIndex === -1) { console.error(`${LOG_PREFIX} 无法确定当前同类型兄弟节点的位置`); return false; } // 3.3 计算位置差 const diff = targetIndex - currentIndex; console.log(`${LOG_PREFIX} 需要切换 ${diff} 步 (目标位置: ${targetIndex}, 当前位置: ${currentIndex})`); if (diff === 0) { console.log(`${LOG_PREFIX} 已经在目标位置`); return true; } // 4. 执行切换操作 const direction = diff > 0 ? 'right' : 'left'; const steps = Math.abs(diff); // 找到要操作的前端节点索引 let frontendNodeIndex = -1; if (parentUuid === Config.INITIAL_PARENT_UUID) { // 根节点场景:找到第一个同类型节点 for (let i = 0; i < ClaudeAPI.currentLinearBranch.length; i++) { const node = ClaudeAPI.currentLinearBranch[i]; if (node && node.sender === targetNode.sender) { frontendNodeIndex = i + 1; // 转换为1基索引 break; } } } else { // 非根节点场景:找到父节点后的第一个同类型节点 const parentIndexInBranch = ClaudeAPI.currentLinearBranch.findIndex(node => node && node.uuid === parentUuid ); if (parentIndexInBranch !== -1) { for (let i = parentIndexInBranch + 1; i < ClaudeAPI.currentLinearBranch.length; i++) { const node = ClaudeAPI.currentLinearBranch[i]; if (node && node.sender === targetNode.sender) { frontendNodeIndex = i + 1; // 转换为1基索引 break; } } } } if (frontendNodeIndex === -1) { console.error(`${LOG_PREFIX} 无法确定要操作的前端节点索引`); return false; } // 执行切换步骤 for (let step = 0; step < steps; step++) { console.log(`${LOG_PREFIX} 执行第 ${step + 1}/${steps} 步 ${direction} 切换`); const success = await this.clickNodeSwitch(direction, frontendNodeIndex); if (!success) { console.error(`${LOG_PREFIX} 第 ${step + 1} 步切换失败`); return false; } // 等待切换完成 await this.wait(300); // 更新当前分支状态 await ClaudeAPI.updateCurrentLinearBranch(); } // 5. 验证是否切换成功 await this.wait(200); await ClaudeAPI.updateCurrentLinearBranch(); const success = ClaudeAPI.isNodeInCurrentBranch(targetNodeUuid); console.log(`${LOG_PREFIX} 切换${success ? '成功' : '失败'}: ${targetNodeUuid.slice(-8)}`); return success; }, // 递归切换到目标节点 async switchToTargetNode(targetNodeUuid) { console.log(`${LOG_PREFIX} 开始切换到目标节点: ${targetNodeUuid.slice(-8)}`); // 确保对话树已初始化 if (!ClaudeAPI.conversationTree) { await ClaudeAPI.tryInitializeConversationTree(); if (!ClaudeAPI.conversationTree) { console.error(`${LOG_PREFIX} 对话树未初始化`); return false; } } // 更新当前分支状态 await ClaudeAPI.updateCurrentLinearBranch(); // 尝试直接切换到目标节点 if (await this.switchToTargetStageNode(targetNodeUuid)) { return true; } // 如果直接切换失败,递归切换到父节点 const { nodes } = ClaudeAPI.conversationTree; const targetNode = nodes[targetNodeUuid]; if (!targetNode) { console.error(`${LOG_PREFIX} 找不到目标节点: ${targetNodeUuid.slice(-8)}`); return false; } // 检查目标节点是否为脏数据 if (targetNode._isDirtyData) { console.error(`${LOG_PREFIX} 目标节点是脏数据,无法递归切换: ${targetNodeUuid.slice(-8)}`); return false; } const parentUuid = targetNode.parent_message_uuid; if (!parentUuid || parentUuid === Config.INITIAL_PARENT_UUID) { console.error(`${LOG_PREFIX} 已到达根节点,无法继续递归`); return false; } console.log(`${LOG_PREFIX} 递归切换到父节点: ${parentUuid.slice(-8)}`); // 递归调用切换到父节点 if (await this.switchToTargetNode(parentUuid)) { // 父节点切换成功后,再次尝试切换到目标节点 console.log(`${LOG_PREFIX} 父节点切换成功,重新尝试切换到目标节点`); return await this.switchToTargetStageNode(targetNodeUuid); } else { console.error(`${LOG_PREFIX} 递归失败,无法切换到父节点: ${parentUuid.slice(-8)}`); return false; } }, // 通用切换函数(简化版,用于与现有按钮交互) async clickNodeSwitch(direction, frontendIndex = 1) { const turns = ClaudeAPI.findCurrentTurns(); if (frontendIndex < 1 || frontendIndex > turns.length) { console.error(`${LOG_PREFIX} 前端节点索引超出范围。有效范围: 1-${turns.length}`); return false; } // 在指定的前端节点中查找切换按钮 const targetTurn = turns[frontendIndex - 1]; let buttonSelector; if (direction === 'right') { buttonSelector = 'button[type="button"]:not([disabled]) svg path[d*="M6.13378 3.16011"]'; } else if (direction === 'left') { buttonSelector = 'button[type="button"] svg path[d*="M13.2402 3.07224"]'; } else { console.error(`${LOG_PREFIX} 无效的方向参数。请使用 'left' 或 'right'`); return false; } const buttonPath = targetTurn.querySelector(buttonSelector); if (buttonPath) { const button = buttonPath.closest('button'); if (button && !button.disabled) { console.log(`${LOG_PREFIX} 点击前端节点#${frontendIndex}的${direction === 'right' ? '右' : '左'}切换按钮`); button.click(); await new Promise(resolve => setTimeout(resolve, 200)); return true; } } console.error(`${LOG_PREFIX} 前端节点#${frontendIndex}没有可用的${direction === 'right' ? '右' : '左'}切换按钮`); return false; } }; // ========================================================================= // 6. 线性跳转功能 // ========================================================================= const LinearNavigator = { // 滚动到元素 scrollToElement(element, topMargin = Config.topMargin) { if (!element) return; const anchor = this.findAnchor(element); const scroller = this.getScrollContainer(anchor); if (!scroller) return; const isWindow = scroller === document.documentElement || scroller === document.body || scroller === document.scrollingElement; const scrollerRect = isWindow ? { top: 0, height: window.innerHeight } : scroller.getBoundingClientRect(); const anchorRect = anchor.getBoundingClientRect(); const currentScrollTop = isWindow ? window.scrollY : scroller.scrollTop; const targetScrollTop = currentScrollTop + (anchorRect.top - scrollerRect.top) - topMargin; const maxScrollTop = scroller.scrollHeight - scroller.clientHeight; const finalScrollTop = Math.max(0, Math.min(targetScrollTop, maxScrollTop)); scroller.scrollTo({ top: finalScrollTop, behavior: 'smooth' }); // 高亮效果 this.addHighlight(element); if (anchor !== element) this.addHighlight(anchor); }, findAnchor(turnElement) { const selectors = [ '[data-testid="user-message"]', '.font-claude-response', 'p', 'li', 'pre' ]; for (const selector of selectors) { const element = turnElement.querySelector(selector); if (element && element.offsetParent) return element; } return turnElement; }, getScrollContainer(element) { let el = element; while (el && el !== document.documentElement) { const style = getComputedStyle(el); if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight) { return el; } el = el.parentElement; } return document.scrollingElement || document.documentElement; }, addHighlight(element) { element.classList.add('highlight-pulse'); setTimeout(() => element.classList.remove('highlight-pulse'), 3100); }, // 线性跳转到指定节点 async jumpToNode(nodeUuid) { // 检查目标节点是否为脏数据 if (ClaudeAPI.conversationTree) { const targetNode = ClaudeAPI.conversationTree.nodes[nodeUuid]; if (targetNode && targetNode._isDirtyData) { console.error(`${LOG_PREFIX} 目标节点是脏数据,无法跳转: ${nodeUuid.slice(-8)}`); return false; } } // 首先更新当前线性分支状态 await ClaudeAPI.updateCurrentLinearBranch(); // 2.1 当前前端状态的线性分支包含该节点,直接跳转 if (ClaudeAPI.isNodeInCurrentBranch(nodeUuid)) { this.jumpToNodeInCurrentBranch(nodeUuid); await new Promise(resolve => setTimeout(resolve, 500)); return true; } else { // 2.2 当前前端状态的线性分支不包含该节点,执行跨分支跳转 console.log(`${LOG_PREFIX} 目标节点不在当前分支中,开始跨分支跳转: ${nodeUuid.slice(-8)}`); // 调用分支切换器进行跨分支跳转 const switchSuccess = await BranchSwitcher.switchToTargetNode(nodeUuid); if (switchSuccess) { // 分支切换成功后,执行页面内跳转到目标节点 console.log(`${LOG_PREFIX} 分支切换成功,执行页面内跳转`); await new Promise(resolve => setTimeout(resolve, 300)); // 更新分支状态并跳转 await ClaudeAPI.updateCurrentLinearBranch(); this.jumpToNodeInCurrentBranch(nodeUuid); await new Promise(resolve => setTimeout(resolve, 500)); return true; } else { console.error(`${LOG_PREFIX} 跨分支跳转失败: ${nodeUuid.slice(-8)}`); return false; } } }, jumpToNodeInCurrentBranch(nodeUuid, cachedTurns = null) { // 在当前分支中跳转 const element = document.getElementById(nodeUuid) || this.findElementByNodeUuid(nodeUuid, cachedTurns); if (element) { this.scrollToElement(element); } }, findElementByNodeUuid(nodeUuid, cachedTurns = null) { // 直接通过ID查找 const directElement = document.getElementById(nodeUuid); if (directElement) return directElement; // 基于位置在当前线性分支中查找对应的DOM元素 if (!ClaudeAPI.currentLinearBranch) return null; // 找到目标节点在当前分支中的索引 const nodeIndex = ClaudeAPI.currentLinearBranch.findIndex(node => node.uuid === nodeUuid); if (nodeIndex === -1) return null; // 使用缓存的turns或获取当前显示的所有回合 const turns = cachedTurns || ClaudeAPI.findCurrentTurns(); if (nodeIndex < turns.length) { return turns[nodeIndex]; } return null; } }; // ========================================================================= // 7. 线性对话索引管理 // ========================================================================= const LinearTurnIndex = { generateId(index, urlHash = null) { const hash = urlHash || location.pathname.split('/').pop() || 'default'; return `cpm-ln-turn-${hash}-${index + 1}`; }, detectRole(turnElement) { const isUser = !!turnElement.querySelector('[data-testid="user-message"]'); const isAssistant = !!turnElement.querySelector('.font-claude-response'); if (isUser) return 'user'; if (isAssistant) return 'assistant'; return null; }, build() { const turns = ClaudeAPI.findCurrentTurns(); if (!turns.length) return []; const index = []; for (let i = 0; i < turns.length; i++) { const turnElement = turns[i]; const role = this.detectRole(turnElement); if (!role) continue; turnElement.setAttribute('data-cpm-ln-turn', '1'); const contentElement = turnElement.querySelector('[data-testid="user-message"]') || turnElement.querySelector('.font-claude-response') || turnElement; let preview = TextUtils.getPreview(contentElement); // 简化的附件检测逻辑:用户节点且无文本内容时显示附件图标 if (role === 'user' && !preview) { preview = 'attachment'; } if (!preview) continue; if (!turnElement.id) { turnElement.id = this.generateId(i); } index.push({ id: turnElement.id, idx: i, role, preview, element: turnElement }); } return index; } }; // ========================================================================= // 8. 线性导航UI组件 // ========================================================================= class LinearNavUI { constructor() { this.element = null; this.isHovered = false; this.currentActiveId = null; this.isVisible = false; } create() { this.element = this.createElement(); this.setupDrag(); this.bindEvents(); document.body.appendChild(this.element); return this; } createElement() { const nav = document.createElement('div'); nav.id = 'cpm-ln-nav'; nav.innerHTML = `
${t('linear.title')}
`; return nav; } setupDrag() { const header = this.element.querySelector('.cpm-ln-header'); let isDragging = false, startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target.closest('button')) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = this.element.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; this.element.style.left = `${startLeft + (e.clientX - startX)}px`; this.element.style.top = `${startTop + (e.clientY - startY)}px`; this.element.style.right = 'auto'; this.element.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; }); } bindEvents() { // 悬停状态 this.element.addEventListener('mouseenter', () => { this.isHovered = true; }); this.element.addEventListener('mouseleave', () => { this.isHovered = false; }); // 防止选择 this.element.addEventListener('dblclick', (e) => e.preventDefault(), { capture: true }); this.element.addEventListener('selectstart', (e) => e.preventDefault(), { capture: true }); this.element.addEventListener('mousedown', (e) => { if (e.detail > 1) e.preventDefault(); }, { capture: true }); // 关闭按钮 this.element.querySelector('.cpm-ln-close').addEventListener('click', () => { this.hide(); }); // 刷新 this.element.querySelector('.cpm-ln-refresh').addEventListener('click', () => { this.onRefresh(); }); // 导航按钮 this.element.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.currentTarget.dataset.action; this.onNavigate(action); }); }); // 列表点击 const list = this.element.querySelector('.cpm-ln-list'); list.addEventListener('click', (e) => { const item = e.target.closest('.cpm-ln-item'); if (item && item.dataset.id) { this.onItemClick(item.dataset.id); } }); } show() { if (!this.isVisible) { this.isVisible = true; this.element.classList.add('visible'); StorageManager.setPanelState(true); } } hide() { if (this.isVisible) { this.isVisible = false; this.element.classList.remove('visible'); StorageManager.setPanelState(false); } } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); this.onRefresh(); } } render(indexData) { const list = this.element.querySelector('.cpm-ln-list'); if (!indexData.length) { list.innerHTML = `
${t('linear.empty')}
`; return; } list.innerHTML = ''; for (const item of indexData) { const node = document.createElement('div'); node.className = `cpm-ln-item ${item.role}`; node.dataset.id = item.id; // 检查是否为附件格式并添加图标 const hasAttachmentFormat = item.preview === 'attachment'; if (hasAttachmentFormat) { node.innerHTML = ` ${item.idx + 1}. ${escapeHTML(item.preview)} `; } else { node.innerHTML = ` ${item.idx + 1}. ${escapeHTML(item.preview)} `; } node.setAttribute('draggable', 'false'); list.appendChild(node); } } setActive(id) { this.currentActiveId = id; const list = this.element.querySelector('.cpm-ln-list'); list.querySelectorAll('.cpm-ln-item.active').forEach(n => n.classList.remove('active')); const activeItem = list.querySelector(`.cpm-ln-item[data-id="${id}"]`); if (activeItem) { activeItem.classList.add('active'); // 确保激活项可见 const itemRect = activeItem.getBoundingClientRect(); const listRect = list.getBoundingClientRect(); if (itemRect.top < listRect.top) { list.scrollTop += itemRect.top - listRect.top - 4; } else if (itemRect.bottom > listRect.bottom) { list.scrollTop += itemRect.bottom - listRect.bottom + 4; } } } destroy() { if (this.element) { this.element.remove(); this.element = null; } } // 事件回调(由外部设置) onRefresh() {} onNavigate() {} onItemClick() {} } // ========================================================================= // 9. 共享UI与逻辑模块 // ========================================================================= const SharedLogic = { async renderTreeView(container, messages, options = {}) { const { isForBranching = false, isNavigationMode = false, onNodeClick = () => {} } = options; container.innerHTML = ''; if (!messages || messages.length === 0) { container.innerHTML = `

${t('tree.empty')}${isForBranching ? t('tree.emptyForBranching') : ''}。

`; return; } if (isForBranching && !isNavigationMode) { const rootBtn = document.createElement('div'); rootBtn.id = 'cpm-branch-from-root-btn'; rootBtn.textContent = t('navigator.branchFromRoot'); rootBtn.onclick = () => onNodeClick(Config.INITIAL_PARENT_UUID, rootBtn); container.appendChild(rootBtn); } const { nodes, childrenMap, rootNodes } = ClaudeAPI.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 * 0}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 += `
└─ [${t('tree.attachments')}]:
'; } // 检测是否为脏数据节点 const isDirtyNode = node._isDirtyData || (node.tree_id && node.tree_id.includes('-F')); const dirtyClass = isDirtyNode ? ' cpm-dirty-node' : ''; const dirtyLabel = isDirtyNode ? ` [${t('tree.dirtyData')}]` : ''; // 使用现代化DOM操作,避免HTML注入 const header = document.createElement('div'); header.className = `cpm-tree-node-header${dirtyClass}`; const idSpan = document.createElement('span'); idSpan.className = 'cpm-tree-node-id'; idSpan.textContent = `[${node.tree_id}]`; const senderSpan = document.createElement('span'); senderSpan.className = `cpm-tree-node-sender sender-${sender.toLowerCase()}`; senderSpan.textContent = `${sender}${retryMarker}${dirtyLabel}:`; const previewSpan = document.createElement('span'); previewSpan.className = 'cpm-tree-node-preview'; previewSpan.textContent = preview || t('tree.attachmentOrToolOnly'); header.append(idSpan, senderSpan, previewSpan); nodeElement.appendChild(header); // 附件HTML部分仍需要innerHTML(因为包含复杂HTML结构) if (attachmentsHTML) { const attachmentsDiv = document.createElement('div'); attachmentsDiv.innerHTML = attachmentsHTML; nodeElement.appendChild(attachmentsDiv); } // 根据模式决定哪些节点可以点击(脏数据节点不可点击) const isClickable = !isDirtyNode && (isNavigationMode ? true : (isForBranching && node.sender === 'assistant')); if (isClickable) { nodeElement.classList.add('cpm-node-clickable'); nodeElement.title = isNavigationMode ? t('navigator.clickToNavigate') : t('navigator.clickToContinue'); 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 } = ClaudeAPI.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, ...file })); (node.files || []).forEach(file => allAttachments.push({ type: 'binary', ...file })); (node.files_v2 || []).forEach(file => allAttachments.push({ type: 'binary', ...file })); } if (allAttachments.length > 0) { statusCallback(t('export.foundAttachments', 'export.foundAttachments', allAttachments.length), 'info'); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error(t('export.cannotGetOrgInfo')); for (let i = 0; i < allAttachments.length; i++) { const file = allAttachments[i]; let fileName; // 双分割策略处理文件名 let extensionForCheck; // 用于检查的部分 (最后一个点) let baseNameForRestore, extensionForRestore; // 用于还原的部分 (第一个点) if (file.file_name && file.file_name.includes('.')) { // 按最后一个点分割 - 用于检查扩展名类型 const lastDotIndex = file.file_name.lastIndexOf('.'); extensionForCheck = file.file_name.substring(lastDotIndex); // 按第一个点分割 - 用于还原完整扩展名 const firstDotIndex = file.file_name.indexOf('.'); baseNameForRestore = file.file_name.substring(0, firstDotIndex); extensionForRestore = file.file_name.substring(firstDotIndex); } else { // 没有扩展名的情况 baseNameForRestore = file.file_name || 'unknown_file'; extensionForCheck = extensionForRestore = ''; } if (file.type === 'text') { // 统一使用第一个点分割的结果构造文件名 fileName = `${baseNameForRestore}_[${file.id || 'no-id'}]${extensionForRestore}`; // 对于以下列表中的文件类型,添加.txt后缀 if (extensionForCheck && ( Config.ContentExtractorHandler.includes(extensionForCheck.toLowerCase()) || Config.SpecialContent.includes(extensionForCheck.toLowerCase()) || Config.PdfHandler.includes(extensionForCheck.toLowerCase()) || Config.OutOfContentFileHandler.includes(extensionForCheck.toLowerCase()) )) { fileName += '.txt'; } } else if (file.type === 'binary' && file.file_uuid) { // 二进制文件使用第一个点分割的结果,保留完整扩展名 fileName = `${baseNameForRestore}_[${file.file_uuid}]${extensionForRestore}`; } if (!fileName) continue; try { await exportDirHandle.getFileHandle(fileName, { create: false }); statusCallback(t('export.skipExistingFile', 'export.skipExistingFile', i + 1, allAttachments.length, fileName), 'info'); continue; } catch (error) { if (error.name !== 'NotFoundError') { console.error(t('error.checkingFile', 'error.checkingFile', fileName) + ':', error); statusCallback(t('status.checkingFile').replace('{0}', fileName), 'error'); continue; } } statusCallback(t('export.downloading', 'export.downloading', 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('.') ? rsplit(file.file_name, '.', 1)[1] : ''; downloadUrl = `/api/${orgInfo.uuid}/files/${file.file_uuid}/document_${ext.replace('.','')}/${file.file_name}`; } if(!downloadUrl) throw new Error(t('export.noDownloadUrl')); 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(t('export.processAttachmentFailed', 'export.processAttachmentFailed', fileName), 'error'); } } } }, async performExportOriginal(convUuid, statusCallback) { if (typeof window.showDirectoryPicker !== 'function') throw new Error(t('error.browserNotSupported')); statusCallback(t('export.requestingFolder'), 'info'); let rootDirHandle; try { rootDirHandle = await window.showDirectoryPicker(); } catch (err) { if (err.name === 'AbortError') { statusCallback(t('export.userCancelled'), 'info', 3000); return; } throw err; } try { const historyData = await ClaudeAPI.getConversationHistory(convUuid); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error(t('export.orgInfoRequired')); statusCallback(t('export.creatingDirectory'), '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(t('status.writingFile').replace('{0}', 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(t('export.originalComplete'), 'success', 5000); } catch (error) { console.error("原始导出失败:", error); statusCallback(`${t('export.originalFailed')}: ${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(t('error.browserNotSupported')); statusCallback(t('export.requestingFolder'), 'info'); let rootDirHandle; try { rootDirHandle = await window.showDirectoryPicker(); } catch (err) { if (err.name === 'AbortError') { statusCallback(t('export.userCancelled'), 'info', 3000); return; } throw err; } try { const historyData = await ClaudeAPI.getConversationHistory(convUuid); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error(t('export.orgInfoRequired')); statusCallback(t('export.creatingDirectory'), '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(t('status.convertingData'), '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(t('status.writingFile').replace('{0}', 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(t('exportStatus.customComplete'), 'success', 5000); } catch (error) { console.error(t('export.customFailed') + ':', error); statusCallback(`${t('exportStatus.customFailed')}: ${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(t('error.insufficientRounds')); 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(t('error.noValidTextContent')); 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, isManagerButtonVisible: true, keydownHandler: null, essentialElementIds: ['cpm-manager-button', 'cpm-main-panel', 'cpm-settings-panel', 'cpm-tree-panel'], init() { const uiIntact = this.hasEssentialElements(); if (this.isInitialized && uiIntact) return; if (this.isInitialized && !uiIntact) { console.warn(LOG_PREFIX, "检测到管理器UI节点缺失,准备重新挂载。"); this.destroyUI(); this.isInitialized = false; } if (!this.isInitialized) { this.createUI(); this.bindEvents(); ClaudeAPI.getOrgUuid().catch(err => console.error(LOG_PREFIX, "预获取OrgId失败", err)); this.isInitialized = true; console.log(LOG_PREFIX, "主管理器UI已初始化。"); } }, hasEssentialElements() { return this.essentialElementIds.every(id => document.getElementById(id)); }, destroyUI() { this.essentialElementIds.forEach(id => document.getElementById(id)?.remove()); document.querySelector('div[data-cpm="svg-defs"]')?.remove(); document.querySelectorAll('.cpm-modal-overlay').forEach(el => el.remove()); if (this.keydownHandler) { document.removeEventListener('keydown', this.keydownHandler); this.keydownHandler = null; } }, createUI() { const svgDefs = document.createElement('div'); svgDefs.dataset.cpm = 'svg-defs'; svgDefs.style.display = 'none'; svgDefs.innerHTML = ` bat bat `; document.body.appendChild(svgDefs); const managerButton = document.createElement('button'); managerButton.id = 'cpm-manager-button'; managerButton.innerHTML = t('manager.title'); managerButton.title = t('tooltip.managerButton'); managerButton.style.display = this.isManagerButtonVisible ? 'block' : 'none'; document.body.appendChild(managerButton); const mainPanel = document.createElement('div'); mainPanel.id = 'cpm-main-panel'; mainPanel.className = 'cpm-panel'; mainPanel.innerHTML = `

${t('manager.title')}

${t('manager.refreshButtonTip')}

${t('manager.ready')}
`; document.body.appendChild(mainPanel); const settingsPanel = document.createElement('div'); settingsPanel.id = 'cpm-settings-panel'; settingsPanel.className = 'cpm-panel'; const settingsHeader = `

${t('settings.title')}

`; 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 = `

${module.title}

` + module.render(); settingsContent.appendChild(section); } const settingsButtons = `
`; 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 = `

${t('tree.preview')}

${t('tree.loading')}

`; 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-batch-export-original').onclick = () => this.handleBatchExport('original'); document.getElementById('cpm-batch-export-custom').onclick = () => this.handleBatchExport('custom'); document.getElementById('cpm-save-settings-button').onclick = () => this.saveSettings(); document.getElementById('cpm-tree-close-button').onclick = () => this.hidePanel('cpm-tree-panel'); if (!this.keydownHandler) { // 添加Ctrl+M键盘快捷键监听 this.keydownHandler = (e) => { if (e.ctrlKey && e.key === 'm') { e.preventDefault(); this.toggleManagerButtonVisibility(); } }; document.addEventListener('keydown', this.keydownHandler); } 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(t('settings.saved'), 'success', 3000); this.togglePanel('cpm-main-panel'); }, async loadConversations() { const listContainer = document.querySelector('#cpm-main-panel .cpm-list-container'); listContainer.innerHTML = `

${t('manager.loading')}

`; this.updateStatus(t('manager.loading'), 'info'); try { const convos = await ManagerService.loadConversations(); this.renderConversationList(); this.updateStatus(`${t('status.loadedSessions')} ${convos.length} ${t('exportStatus.sessions')}。`, 'info'); } catch (error) { listContainer.innerHTML = `

${t('error.loadSessionsFailed')}: ${error.message}

`; this.updateStatus(t('status.loadFailed'), '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 = `

${t('manager.noResults')}

`; 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 || t('treeView.untitled'); let highlightedTitle = titleText; if (this.currentSearch) highlightedTitle = titleText.replace(new RegExp(this.escapeRegExp(this.currentSearch), 'gi'), (match) => `${match}`); const star = convo.is_starred ? '' : ''; li.innerHTML = `
${star}${highlightedTitle}${new Date(convo.updated_at).toLocaleString()}
`; 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 = ``; actionsDiv.innerHTML = ``; 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>/)[1].replace(/<[^>]*>/g, '').replace(/★/g, '').trim(); if (!newTitle || newTitle === originalTitle) { this.exitEditMode(li); return; } input.disabled = true; this.updateStatus(t('status.savingTitle'), 'info'); try { await ManagerService.performManualRename(uuid, newTitle); this.updateStatus(t('status.saveSuccess'), 'success'); const convo = ManagerService.conversationsCache.find(c => c.uuid === uuid); const star = convo.is_starred ? '' : ''; li.dataset.originalDetails = li.dataset.originalDetails.replace(/>(★)?.*?<\/span>/, `>${star}${newTitle}`); this.exitEditMode(li); } catch (error) { this.updateStatus(`${t('status.saveFailed')}: ${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 = `${t('treeView.prefix')}${convo ? (convo.name || t('treeView.untitled')) : t('treeView.loading')}`; treeContainer.innerHTML = `

${t('navigator.loadingHistory')}

`; treePanel.style.display = 'flex'; try { const historyData = await ClaudeAPI.getConversationHistory(uuid); // 使用经过脏数据标记处理的消息数据 const processedMessages = ClaudeAPI.markDirtyMessages(historyData.chat_messages); await SharedLogic.renderTreeView(treeContainer, processedMessages); } catch (error) { console.error(error); treeContainer.innerHTML = `

${t('error.cannotLoadTree')}: ${error.message}

`; } }, 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 = t('status.ready'); s.classList.remove('is-error', 'is-success'); }, timeout); } }, async handleBatchOperation(opName, serviceFunc, opType, ...args) { const uuids = this.getSelectedUuids(); if (uuids.length === 0) { alert(t('error.selectSessions').replace('{0}', opName)); return; } if (opType === 'delete' && !confirm(t('batchOps.confirmDelete').replace('{0}', uuids.length))) return; document.querySelectorAll('.cpm-action-btn').forEach(btn => btn.disabled = true); this.updateStatus(t('status.batchProcessing').replace('{0}', opName).replace('{1}', uuids.length), 'info'); let successCount = 0; try { if (opType === 'rename') { for (let i = 0; i < uuids.length; i++) { this.updateStatus(t('status.batchItemProcessing').replace('{0}', opName).replace('{1}', i + 1).replace('{2}', 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(`${t('status.batchItemFailed').replace('{0}', 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(t('status.batchOperationComplete').replace('{0}', opName).replace('{1}', successCount).replace('{2}', uuids.length), 'success', 4000); } catch(e) { this.updateStatus(`${t('status.batchOperationFailed').replace('{0}', opName)}: ${e.message}`, 'error', 5000); } const refreshSettingKey = opType === 'delete' ? 'refreshAfterDelete' : opType === 'star' ? 'refreshAfterStar' : 'refreshAfterRename'; if (GM_getValue(refreshSettingKey, false)) { this.updateStatus(document.querySelector('#cpm-main-panel .cpm-status-bar').textContent + t('status.refreshingFromServer'), 'info'); await this.loadConversations(); } else { this.renderConversationList(); } document.querySelectorAll('.cpm-action-btn').forEach(btn => btn.disabled = false); }, handleBatchRename() { this.handleBatchOperation(t('operation.rename'), ManagerService.performAutoRename.bind(ManagerService), 'rename'); }, handleBatchDelete() { this.handleBatchOperation(t('operation.delete'), ManagerService.performBatchDelete.bind(ManagerService), 'delete'); }, handleBatchStar(isStarring) { this.handleBatchOperation(isStarring ? t('operation.star') : t('operation.unstar'), ManagerService.performBatchStarAction.bind(ManagerService), 'star', isStarring); }, handleBatchExport(type) { const uuids = this.getSelectedUuids(); if (uuids.length === 0) { alert(t('error.selectExportSessions')); return; } if (type === 'original') { this.performBatchExportOriginal(uuids); } else if (type === 'custom') { this.showBatchExportModal(uuids); } }, async performBatchExportOriginal(uuids) { if (typeof window.showDirectoryPicker !== 'function') { alert(t('error.browserNotSupported')); return; } this.updateStatus(t('status.batchExportPreparing').replace('{0}', uuids.length), 'info'); let rootDirHandle; try { rootDirHandle = await window.showDirectoryPicker(); } catch (err) { if (err.name === 'AbortError') { this.updateStatus(t('export.userCancelled'), 'info', 3000); return; } throw err; } let successCount = 0; for (let i = 0; i < uuids.length; i++) { const uuid = uuids[i]; const convo = ManagerService.conversationsCache.find(c => c.uuid === uuid); const title = convo ? (convo.name || t('treeView.untitled')) : t('treeView.loading'); this.updateStatus(t('export.exportingProgress', 'export.exportingProgress', i + 1, uuids.length, title), 'info'); try { await this.exportSingleConversation(uuid, rootDirHandle, 'original'); successCount++; } catch (error) { console.error(t('export.sessionFailed', 'export.sessionFailed', uuid) + ':', error); this.updateStatus(t('export.exportFailed', 'export.exportFailed', i + 1, uuids.length, error.message), 'error'); await new Promise(resolve => setTimeout(resolve, 2000)); } if (i < uuids.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } this.updateStatus(t('export.batchComplete', 'export.batchComplete', successCount, uuids.length), 'success', 5000); }, async exportSingleConversation(uuid, rootDirHandle, type) { const historyData = await ClaudeAPI.getConversationHistory(uuid); const orgInfo = await ClaudeAPI.getOrganizationInfo(); if (!orgInfo) throw new Error(t('export.orgInfoRequired')); const orgName = (orgInfo.name || "unknown_org").replace(/'s Organization$/, ""); const safeTitle = (historyData.name || "Untitled").replace(/[<>:"/\\|?*]/g, '_'); const pathParts = [`Claude_Exports`, `[${orgName}]`, `[${type === 'original' ? 'Original' : 'Custom'}]_[${safeTitle}]_[${uuid}]`]; 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`; let dataToWrite; if (type === 'original') { dataToWrite = historyData; } else if (type === 'custom') { const settings = this.tempBatchExportSettings; dataToWrite = ManagerService.transformConversation(historyData, settings); } const historyFileHandle = await exportDirHandle.getFileHandle(historyFileName, { create: true }); const writableHistory = await historyFileHandle.createWritable(); await writableHistory.write(JSON.stringify(dataToWrite, null, 2)); await writableHistory.close(); if (type === 'original' || (type === 'custom' && this.tempBatchExportSettings.attachments.mode !== 'none')) { await ManagerService.exportAttachmentsForConversation(historyData, exportDirHandle, () => {}); } }, showBatchExportModal(uuids) { 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 = `

${t('exportSettings.batchCustomOptions')} (${uuids.length} ${t('exportStatus.sessions')})

${this.createExportSettingsHTML(false)}
`; 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-batch-export-now-btn').onclick = async () => { try { const currentSettings = this.getExportSettings(modalContent); this.tempBatchExportSettings = currentSettings; modalContent.querySelector('#cpm-batch-export-now-btn').disabled = true; modalContent.querySelector('#cpm-batch-export-now-btn').textContent = t('status.preparingExport'); overlay.remove(); await this.performBatchExportCustom(uuids); } catch (error) { console.error(`${LOG_PREFIX} 批量导出失败:`, error); this.updateStatus(`${t('status.batchExportFailed')}: ${error.message}`, 'error'); } }; }, async performBatchExportCustom(uuids) { if (typeof window.showDirectoryPicker !== 'function') { alert(t('error.browserNotSupported')); return; } this.updateStatus(`${t('exportStatus.batchPreparing')} ${uuids.length} ${t('exportStatus.sessions')}...`, 'info'); let rootDirHandle; try { rootDirHandle = await window.showDirectoryPicker(); } catch (err) { if (err.name === 'AbortError') { this.updateStatus(t('export.userCancelled'), 'info', 3000); return; } throw err; } let successCount = 0; for (let i = 0; i < uuids.length; i++) { const uuid = uuids[i]; const convo = ManagerService.conversationsCache.find(c => c.uuid === uuid); const title = convo ? (convo.name || t('treeView.untitled')) : t('treeView.loading'); this.updateStatus(t('export.exportingProgress', 'export.exportingProgress', i + 1, uuids.length, title), 'info'); try { await this.exportSingleConversation(uuid, rootDirHandle, 'custom'); successCount++; } catch (error) { console.error(t('export.sessionFailed', 'export.sessionFailed', uuid) + ':', error); this.updateStatus(t('export.exportFailed', 'export.exportFailed', i + 1, uuids.length, error.message), 'error'); await new Promise(resolve => setTimeout(resolve, 2000)); } if (i < uuids.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } delete this.tempBatchExportSettings; this.updateStatus(t('export.batchComplete').replace('{0}', successCount).replace('{1}', uuids.length), 'success', 5000); }, createExportSettingsHTML(forSettingsPanel = false) { const maybeRemoveTitle = forSettingsPanel ? '' : `

${t('settings.exportDefaults')}

`; return ` ${maybeRemoveTitle}

${t('exportSettings.basicInfo')}

${t('exportSettings.messageStructure')}

${t('exportSettings.timestampInfo')}

${t('exportSettings.coreContent')}

${t('exportSettings.advancedContent')}

`; }, 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 = `

${t('exportSettings.customOptions')}

${this.createExportSettingsHTML(false)}
`; 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 () => { try { const currentSettings = this.getExportSettings(modalContent); modalContent.querySelector('#cpm-export-now-btn').disabled = true; modalContent.querySelector('#cpm-export-now-btn').textContent = t('status.exporting'); await ManagerService.performExportCustom(uuid, currentSettings, this.updateStatus.bind(this)); overlay.remove(); } catch (error) { console.error(`${LOG_PREFIX} 导出失败:`, error); this.updateStatus(`${t('status.exportFailed')}: ${error.message}`, 'error'); modalContent.querySelector('#cpm-export-now-btn').disabled = false; modalContent.querySelector('#cpm-export-now-btn').textContent = t('exportSettings.exportNow'); } }; }, toggleManagerButtonVisibility() { this.isManagerButtonVisible = !this.isManagerButtonVisible; const managerButton = document.getElementById('cpm-manager-button'); if (!managerButton) { console.warn(LOG_PREFIX, "Manager按钮缺失,尝试重新挂载UI。"); this.init(); return; } managerButton.style.display = this.isManagerButtonVisible ? 'block' : 'none'; console.log(LOG_PREFIX, `Manager按钮已${this.isManagerButtonVisible ? '显示' : '隐藏'} (Ctrl+M)`); } }; // ========================================================================= // 8. 聊天增强模块 (Enhancer Modules) // ========================================================================= const NavigatorEnhancer = { state: { conversationUUID: null, selectedParentMessageUUID: null, currentMode: 'branch' // 'branch' 或 'navigate' }, init() { this.cleanup(); this.createNavigatorButton(); }, 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(); }, createNavigatorButton() { 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 = t('tooltip.navigatorButton'); 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 = `
`; 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-navigator-panel-override'; modalContent.style.display = 'flex'; modalContent.onclick = (e) => e.stopPropagation(); modalContent.innerHTML = `

${t('navigator.title')}

`; overlay.appendChild(modalContent); document.body.appendChild(overlay); // 绑定事件 overlay.querySelector('#cpm-navigator-modal-close-btn').onclick = () => overlay.remove(); overlay.querySelector('#cpm-branch-mode-btn').onclick = () => this.switchMode('branch', modalContent); overlay.querySelector('#cpm-navigate-mode-btn').onclick = () => this.switchMode('navigate', modalContent); // 加载内容 await this.loadModalContent(modalContent); }, switchMode(mode, modalContent) { this.state.currentMode = mode; modalContent.querySelector('.cpm-mode-btn.active')?.classList.remove('active'); modalContent.querySelector(`#cpm-${mode === 'branch' ? 'branch' : 'navigate'}-mode-btn`).classList.add('active'); this.loadModalContent(modalContent); }, async loadModalContent(modalContent) { const treeContainer = modalContent.querySelector('#cpm-navigator-tree-container'); if (this.state.conversationUUID) { treeContainer.innerHTML = `

${t('navigator.loading')}

`; try { // 使用智能缓存机制,避免重复请求 await ClaudeAPI.tryInitializeConversationTree(); if (!ClaudeAPI.conversationTree) { throw new Error(t('error.cannotGetConvoData')); } // 从缓存的对话树获取消息数据 const messages = Object.values(ClaudeAPI.conversationTree.nodes); await SharedLogic.renderTreeView(treeContainer, messages, { isForBranching: this.state.currentMode === 'branch', isNavigationMode: this.state.currentMode === 'navigate', onNodeClick: (uuid, element) => this.handleNodeClick(uuid, element) }); } catch (error) { treeContainer.innerHTML = `

${t('status.loadFailed')}: ${error.message}

`; } } else { treeContainer.innerHTML = `

${t('navigator.notInChat')}

`; } }, handleNodeClick(uuid, element) { if (this.state.currentMode === 'branch') { // 延续模式:设置分支点 this.selectBranchPoint(uuid, element); } else { // 导航模式:直接跳转 this.navigateToNode(uuid, element); } }, selectBranchPoint(uuid, element) { this.state.selectedParentMessageUUID = uuid; document.querySelectorAll('.cpm-node-selected').forEach(n => n.classList.remove('cpm-node-selected')); element.classList.add('cpm-node-selected'); this.updateStatusIndicator(); setTimeout(() => document.querySelector('.cpm-modal-overlay')?.remove(), 300); }, async navigateToNode(uuid, element) { document.querySelectorAll('.cpm-node-selected').forEach(n => n.classList.remove('cpm-node-selected')); element.classList.add('cpm-node-selected'); // 立即关闭面板 setTimeout(() => document.querySelector('.cpm-modal-overlay')?.remove(), 300); // 执行导航(跨分支跳转) LinearNavigator.jumpToNode(uuid); }, 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 = t('navigator.branchSelected'); indicator.title = `${t('navigator.nextMessageFrom')}\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 LinearNavEnhancer = { ui: null, currentUrl: location.href, refreshTimer: 0, forceRefreshTimer: null, observer: null, isBooting: false, init() { this.cleanup(); this.createLinearNavigatorButton(); // 检查是否需要自动启动面板 this.checkAutoStart(); }, cleanup() { document.querySelector('#cpm-ln-linear-navigator-btn')?.closest('div.relative.shrink-0').remove(); if (this.ui) { this.ui.destroy(); this.ui = null; } if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.forceRefreshTimer) { clearInterval(this.forceRefreshTimer); this.forceRefreshTimer = null; } if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = 0; } }, createLinearNavigatorButton() { if (document.getElementById('cpm-ln-linear-navigator-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-ln-linear-navigator-btn'; button.type = 'button'; button.title = t('tooltip.linearNavButton'); button.className = "inline-flex items-center justify-center relative shrink-0 can-focus select-none disabled:pointer-events-none 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.style.fontWeight = "normal"; button.innerHTML = `
`; button.onclick = () => this.toggleLinearNavigator(); wrapperDiv.appendChild(button); toolbar.insertBefore(wrapperDiv, emptyArea); }, checkAutoStart() { // 延迟检查,确保页面元素完全加载 const tryAutoStart = (attempt = 0) => { const maxAttempts = 5; const delay = 2000 + (attempt * 1000); // 逐渐增加延迟 setTimeout(() => { // 检查页面是否稳定(工具栏存在且没有正在进行清理) const toolbar = document.querySelector(Config.TOOLBAR_SELECTOR); const hasButton = document.getElementById('cpm-ln-linear-navigator-btn'); if (toolbar && hasButton && StorageManager.getPanelState()) { // 确保不会与现有的UI冲突 if (!this.ui) { this.toggleLinearNavigator(); } } else if (attempt < maxAttempts && toolbar) { // 如果页面还不稳定但工具栏存在,继续尝试 tryAutoStart(attempt + 1); } }, delay); }; tryAutoStart(); }, toggleLinearNavigator() { if (!this.ui) { this.boot(); } this.ui.toggle(); }, boot() { if (this.ui || this.isBooting) return; this.isBooting = true; try { this.ui = new LinearNavUI().create(); this.setupUICallbacks(); this.setupObserver(); this.setupEventListeners(); this.startAutoRefresh(); } finally { this.isBooting = false; } }, setupUICallbacks() { this.ui.onRefresh = () => this.refresh({ ignoreHover: true, force: true }); this.ui.onNavigate = (action) => this.navigate(action); this.ui.onItemClick = (id) => this.jumpToItem(id); }, setupObserver() { if (this.observer) this.observer.disconnect(); this.observer = new MutationObserver(() => { this.refresh({ delay: Config.refreshInterval }); }); this.observer.observe(document.body, { childList: true, subtree: true, attributes: true }); }, setupEventListeners() { // 发送消息后的快速刷新 const handleSend = () => this.burstRefresh(); document.addEventListener('click', (e) => { if (e.target.closest('button[type="submit"], [aria-label*="Send"]')) { handleSend(); } }, true); document.addEventListener('keydown', (e) => { const target = e.target; if ((target.tagName === 'TEXTAREA' || target.isContentEditable) && e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { handleSend(); } }, true); document.addEventListener('visibilitychange', () => { if (!document.hidden) this.refresh({ force: true }); }); }, startAutoRefresh() { if (this.forceRefreshTimer) clearInterval(this.forceRefreshTimer); this.forceRefreshTimer = setInterval(() => { this.refresh({ force: true }); }, 10000); // 10秒自动刷新 }, refresh({ delay = 80, force = false, ignoreHover = false } = {}) { if (this.ui && this.ui.isHovered && !ignoreHover) return; if (force) { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = 0; } this.doRefresh(); return; } if (this.refreshTimer) clearTimeout(this.refreshTimer); this.refreshTimer = setTimeout(() => { this.refreshTimer = 0; this.doRefresh(); }, delay); }, doRefresh() { if (!this.ui) return; try { const indexData = LinearTurnIndex.build(); this.ui.render(indexData); } catch (e) { console.error('Linear refresh error:', e); } }, burstRefresh(duration = 6000, interval = 160) { const endTime = Date.now() + duration; const tick = () => { this.refresh({ force: true, ignoreHover: true }); if (Date.now() < endTime) { setTimeout(tick, interval); } }; tick(); }, navigate(action) { const indexData = LinearTurnIndex.build(); if (!indexData.length) return; if (action === 'top' || action === 'bottom') { const turns = ClaudeAPI.findCurrentTurns(); if (!turns.length) return; const targetTurn = action === 'top' ? turns[0] : turns[turns.length - 1]; const topMargin = action === 'bottom' ? -window.innerHeight : Config.topMargin; LinearNavigator.scrollToElement(targetTurn, topMargin); if (targetTurn && targetTurn.id) { setTimeout(() => this.ui.setActive(targetTurn.id), 150); } return; } const currentIndex = indexData.findIndex(item => item.id === this.ui.currentActiveId); const delta = action === 'prev' ? -1 : 1; let nextIndex; if (currentIndex < 0) { nextIndex = delta > 0 ? 0 : indexData.length - 1; } else { nextIndex = Math.max(0, Math.min(indexData.length - 1, currentIndex + delta)); } const nextItem = indexData[nextIndex]; if (nextItem) { this.jumpToItem(nextItem.id); } }, jumpToItem(id) { const element = document.getElementById(id); if (element) { this.ui.setActive(id); LinearNavigator.scrollToElement(element); } }, updateState(currentUrl) { if (currentUrl !== this.currentUrl) { this.currentUrl = currentUrl; if (this.ui && this.ui.isVisible) { this.refresh({ force: true }); } } } }; 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 = ` `; 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); const slider = document.getElementById('cpm-attachment-mode-toggle-slider'); if (slider) { slider.style.transform = isInitialForceMode ? 'translateX(12px)' : ''; } 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; const isForceMode = toggleSwitch.checked; this.state.forceUploadMode = isForceMode ? 'force' : 'default'; this.updateSubPanelIcon(isForceMode); const slider = document.getElementById('cpm-attachment-mode-toggle-slider'); if (slider) { slider.style.transform = isForceMode ? 'translateX(12px)' : ''; } 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 = `
${t('attachment.title')}
`; 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(() => { 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 = ` ${fileInfo.fileName}

${fileInfo.fileName}

`; 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 || NavigatorEnhancer.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 (NavigatorEnhancer.state.selectedParentMessageUUID) { console.log("执行分支注入..."); payload.parent_message_uuid = NavigatorEnhancer.state.selectedParentMessageUUID; NavigatorEnhancer.state.selectedParentMessageUUID = null; setTimeout(() => NavigatorEnhancer.updateStatusIndicator(), 0); console.log("分支注入完成。"); } options.body = JSON.stringify(payload); } catch (e) { console.error(LOG_PREFIX, "修改/completion请求体失败:", e); } finally { console.groupEnd(); } } } // 执行原始请求 const response = originalFetch.apply(this, args); // 拦截 /completion 和 /retry_completion 的响应 if (url.includes('/completion') || url.includes('/retry_completion')) { return response.then(async (originalResponse) => { try { // 检查响应是否成功 if (originalResponse.ok) { console.log(`%c${LOG_PREFIX} 响应拦截: ${url.includes('/retry_completion') ? '/retry_completion' : '/completion'} 请求成功完成`, 'color: #10b981; font-weight: bold;'); // 延迟清除对话树缓存,确保服务器端数据已更新完成 setTimeout(() => { ClaudeAPI.isInitialized = false; ClaudeAPI.conversationTree = null; ClaudeAPI.currentLinearBranch = null; console.log(`%c${LOG_PREFIX} 已清除对话树缓存,下次导航器访问时将重新获取最新数据`, 'color: #10b981;'); }, 500); // 延迟500ms,等待服务器处理完成 } } catch (error) { console.warn(`${LOG_PREFIX} 响应处理时出错:`, error); } return originalResponse; }).catch((error) => { console.error(`${LOG_PREFIX} 请求失败:`, error); throw error; }); } return response; }; }, startObserver() { this.observer = new MutationObserver(() => this.onPageChange()); this.observer.observe(document.body, { childList: true, subtree: true }); this.onPageChange(); }, cleanup() { if (this.observer) { this.observer.disconnect(); this.observer = null; } ThemeManager.cleanup(); }, onPageChange() { const currentUrl = location.href; // 检测对话切换 ClaudeAPI.checkConversationChange(); const urlChanged = currentUrl !== this.lastUrl; const uiMissing = !ManagerUI.hasEssentialElements(); if (!urlChanged && !uiMissing) { if (document.querySelector(Config.TOOLBAR_SELECTOR) && !document.getElementById('cpm-branch-btn')) { this.setupEnhancers(currentUrl); } return; } if (urlChanged) { this.lastUrl = currentUrl; console.log(LOG_PREFIX, "URL变更或初次加载,执行页面设置。"); } else { console.log(LOG_PREFIX, "检测到脚本UI元素缺失,重新执行页面设置。"); } 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) { NavigatorEnhancer.init(); AttachmentEnhancer.init(); LinearNavEnhancer.init(); NavigatorEnhancer.updateState(currentUrl); LinearNavEnhancer.updateState(currentUrl); } else { NavigatorEnhancer.cleanup(); AttachmentEnhancer.cleanup(); LinearNavEnhancer.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; --cpm-mode-active: #2563eb; --cpm-mode-inactive: #6b7280; } body[cpm-theme='light'] #cpm-back-to-main, body[cpm-theme='light'] #cpm-batch-export-now-btn { color: hsl(var(--cpm-text-000)) !important; } 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-batch-export-btn { background-color: hsl(var(--cpm-bg-300)); border: 1px solid hsl(var(--cpm-border-300)); padding: 8px; border-radius: 6px; } .cpm-batch-export-btn:hover { background-color: hsl(var(--cpm-bg-400)); border-color: hsl(var(--cpm-accent-secondary-100)); } .cpm-batch-export-btn svg { width: 20px !important; height: 20px !important; } #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-navigator-panel-override { width: 90vw; max-width: 1200px; height: 90vh; } .cpm-tree-container { flex-grow: 1; overflow-y: auto; overflow-x: 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; min-width: fit-content; } .cpm-tree-node-header { margin: 0 0 5px 0; display: flex; align-items: baseline; gap: 10px; flex-wrap: nowrap; padding: 4px; white-space: nowrap; } .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)); white-space: nowrap; } .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; } /* --- DIRTY DATA NODE STYLES --- */ .cpm-dirty-node .cpm-tree-node-id { color: hsl(var(--cpm-danger-000)) !important; } .cpm-dirty-node .cpm-tree-node-sender { color: hsl(var(--cpm-danger-000)) !important; } /* --- 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-node-clickable { cursor: pointer; transition: background-color 0.2s; } .cpm-node-clickable:hover, #cpm-branch-from-root-btn:hover { background-color: var(--cpm-branch-hover-bg); } .cpm-node-selected, #cpm-branch-from-root-btn.cpm-node-selected { background-color: var(--cpm-branch-selected-bg) !important; color: var(--cpm-branch-selected-text) !important; } .cpm-node-selected .cpm-tree-node-sender, .cpm-node-selected .cpm-tree-node-preview, .cpm-node-selected .cpm-tree-node-id { color: var(--cpm-branch-selected-text) !important; } /* --- MODE SELECTOR --- */ .cpm-mode-selector { display: flex; gap: 8px; padding: 12px 20px; border-bottom: 1px solid hsl(var(--cpm-border-200)); } .cpm-mode-btn { padding: 8px 16px; border: 1px solid hsl(var(--cpm-border-300)); background: transparent; color: var(--cpm-mode-inactive); border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 14px; font-weight: 500; } .cpm-mode-btn:hover { background-color: hsl(var(--cpm-bg-200)); } .cpm-mode-btn.active { background-color: var(--cpm-mode-active); color: white; border-color: var(--cpm-mode-active); } /* --- 高亮动画 --- */ .highlight-pulse { animation: cpm-highlight-pulse 3s ease-out; } @keyframes cpm-highlight-pulse { 0%, 100% { background-color: rgba(255, 243, 205, 0); } 20% { background-color: rgba(255, 243, 205, 1); } } #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; } /* --- LINEAR NAVIGATION UI --- */ /* 基础容器 */ #cpm-ln-nav { position: fixed; top: 120px; right: 20px; width: auto; min-width: 80px; max-width: 210px; z-index: 2147483647 !important; font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; pointer-events: none; user-select: none; -webkit-tap-highlight-color: transparent; opacity: 0; transform: translateY(-10px); transition: opacity 0.3s ease, transform 0.3s ease; background-color: hsl(var(--cpm-bg-100)); border: 1px solid hsl(var(--cpm-border-300)); border-radius: 8px; box-shadow: 0 10px 25px hsla(var(--cpm-text-000), 0.15); } #cpm-ln-nav.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } #cpm-ln-nav * { user-select: none; } /* 头部区域 */ .cpm-ln-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; margin-bottom: 4px; cursor: move; min-width: 100px; border-bottom: 1px solid hsl(var(--cpm-border-200)); } .cpm-ln-title { font: 600 11px/1 inherit; color: hsl(var(--cpm-text-200)); display: flex; align-items: center; gap: 3px; } .cpm-ln-title svg { width: 12px; height: 12px; } .cpm-ln-close, .cpm-ln-refresh { width: 22px; height: 22px; font-size: 14px; background: none; border: none; color: hsl(var(--cpm-text-400)); cursor: pointer; padding: 3px; border-radius: 4px; transition: color 0.2s, background-color 0.2s; display: flex; align-items: center; justify-content: center; } .cpm-ln-close:hover, .cpm-ln-refresh:hover { color: hsl(var(--cpm-text-100)); background-color: hsl(var(--cpm-bg-200)); } /* 列表区域 */ .cpm-ln-list { max-height: 400px; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; gap: 6px; padding: 8px; } .cpm-ln-list::-webkit-scrollbar { width: 3px; } .cpm-ln-list::-webkit-scrollbar-thumb { background: hsla(var(--cpm-text-400), 0.3); border-radius: 2px; } .cpm-ln-list::-webkit-scrollbar-thumb:hover { background: hsla(var(--cpm-text-400), 0.5); } /* 列表项 */ .cpm-ln-item { padding: 6px 8px; border-radius: 4px; cursor: pointer; transition: all 0.15s ease; font-size: 12px; min-height: 24px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: auto; min-width: 60px; max-width: 190px; background-color: hsl(var(--cpm-bg-200)); } .cpm-ln-item:hover { transform: translateX(2px); box-shadow: 0 2px 6px hsla(var(--cpm-always-black), 0.12); background-color: hsl(var(--cpm-bg-300)); } /* 用户/助手样式 */ .cpm-ln-item.user { color: hsl(var(--cpm-accent-secondary-100)); border-left: 3px solid hsl(var(--cpm-accent-secondary-100)); font-weight: 500; } .cpm-ln-item.assistant { color: hsl(var(--cpm-accent-brand)); border-left: 3px solid hsl(var(--cpm-accent-brand)); font-weight: 500; } .cpm-ln-item.active { border: 2px solid hsl(var(--cpm-accent-pro-100)); box-shadow: 0 2px 8px hsla(var(--cpm-accent-pro-100), 0.2); background-color: hsl(var(--cpm-bg-400)); } .cpm-ln-number { margin-right: 4px; font: 600 11px/1 inherit; color: hsl(var(--cpm-text-400)); } .cpm-ln-empty { padding: 10px; text-align: center; color: hsl(var(--cpm-text-400)); font-size: 11px; min-height: 20px; } /* 上下置顶置底按钮 */ .cpm-ln-footer { margin-top: 8px; display: flex; gap: 4px; padding: 8px; border-top: 1px solid hsl(var(--cpm-border-200)); } /* 导航按钮统一样式(四个按钮共用) */ .cpm-ln-nav-btn { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; padding: 6px; color: hsl(var(--cpm-text-300)); background: hsl(var(--cpm-bg-200)); border: 1px solid hsl(var(--cpm-border-300)); border-radius: 4px; cursor: pointer; transition: all 0.2s; line-height: 1; } .cpm-ln-nav-btn .cpm-svg-icon { width: 14px; height: 14px; } .cpm-ln-nav-btn:hover { color: hsl(var(--cpm-text-100)); border-color: hsl(var(--cpm-accent-secondary-100)); background-color: hsl(var(--cpm-bg-300)); } `); // ========================================================================= // 11. 辅助工具 & 启动脚本 // ========================================================================= App.init(); })(unsafeWindow);