// ==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 `
`;
},
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 = `
`;
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')}]:
`;
allAttachments.forEach(file => {
if (file.type === 'text') {
const contentPreview = (file.extracted_content || '').substring(0, 25);
const escapedPreview = escapeHTML(contentPreview);
attachmentsHTML += `- - ${file.file_name} [Source: convert_document] [ID: ${file.id}] [Preview: "${escapedPreview}..."]
`;
} else {
// 增强URL构造逻辑以支持blob类型
let fullUrl = '';
if (file.document_asset?.url) { // 优先使用显式URL
fullUrl = baseUrl + file.document_asset.url;
} else if (file.preview_url) { // 其次使用预览URL
fullUrl = baseUrl + file.preview_url;
} else if (file.file_kind === 'blob' && orgUuid && file.file_uuid) { // **新增**: 处理 blob 类型
fullUrl = `${baseUrl}/api/organizations/${orgUuid}/files/${file.file_uuid}/contents`;
} else if (orgUuid && file.file_uuid && file.file_name) { // 回退到旧的文档格式
const ext = file.file_name.includes('.') ? rsplit(file.file_name, '.', 1)[1] : '';
if (ext) fullUrl = `${baseUrl}/api/${orgUuid}/files/${file.file_uuid}/document_${ext.replace('.','')}/${file.file_name}`;
}
const urlLink = fullUrl ? `[View/Download URL]` : '[URL Not Available]';
attachmentsHTML += `- - ${file.file_name} [Source: /upload | Type: ${file.file_kind || 'unknown'}] ${urlLink}
`;
}
});
attachmentsHTML += '
';
}
// 检测是否为脏数据节点
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 = `
`;
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.refreshButtonTip')}
${t('manager.ready')}
`;
document.body.appendChild(mainPanel);
const settingsPanel = document.createElement('div');
settingsPanel.id = 'cpm-settings-panel';
settingsPanel.className = 'cpm-panel';
const settingsHeader = ``;
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 = `
`;
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 = `
${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}
`;
},
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 = `
${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 = `
`;
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 = `
`;
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 = `
`;
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);