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