您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add additional metadata to Claude messages: index, branch, timestamp, UUID
当前为
// ==UserScript== // @name Claude Message Info // @namespace http://tampermonkey.net/ // @version 0.0.9 // @description Add additional metadata to Claude messages: index, branch, timestamp, UUID // @author MRL // @match https://claude.ai/* // @license MIT // ==/UserScript== (function() { 'use strict'; // ============================================= // API FUNCTIONS // ============================================= /** * Extracts conversation ID from current URL */ function getConversationId() { const match = window.location.pathname.match(/\/chat\/([^/?]+)/); return match ? match[1] : null; } /** * Gets organization ID from browser cookies */ function getOrgId() { const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'lastActiveOrg') { return value; } } throw new Error('Could not find organization ID'); } /** * Fetches conversation data from Claude API */ async function getConversationData() { const conversationId = getConversationId(); if (!conversationId) { throw new Error('Not in a conversation'); } const orgId = getOrgId(); const response = await fetch( `/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true` ); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } return await response.json(); } // ============================================= // BRANCH BUILDING FUNCTIONS // ============================================= /** * Builds conversation tree structure */ function buildConversationTree(messages) { const messageMap = new Map(); const rootMessages = []; // Create message map messages.forEach(message => { messageMap.set(message.uuid, { ...message, children: [] }); }); // Build parent-child relationships messages.forEach(message => { const messageNode = messageMap.get(message.uuid); const parentUuid = message.parent_message_uuid; if (parentUuid && parentUuid !== "00000000-0000-4000-8000-000000000000" && messageMap.has(parentUuid)) { const parent = messageMap.get(parentUuid); parent.children.push(messageNode); } else { rootMessages.push(messageNode); } }); return { messageMap, rootMessages }; } /** * Finds main branch path from current_leaf_message_uuid */ function findMainBranchPathFromLeaf(tree, currentLeafUuid) { if (!currentLeafUuid) { return []; } const mainPath = []; let currentMessage = tree.messageMap.get(currentLeafUuid); while (currentMessage) { mainPath.unshift(currentMessage); // Add to beginning const parentUuid = currentMessage.parent_message_uuid; if (parentUuid === "00000000-0000-4000-8000-000000000000" || !parentUuid) { break; } currentMessage = tree.messageMap.get(parentUuid); } return mainPath; } /** * Finds main branch path from message with maximum index */ function findMainBranchPathFromMaxIndex(tree) { // Find message with maximum index let maxIndexMessage = null; let maxIndex = -1; tree.messageMap.forEach(message => { if (message.index > maxIndex) { maxIndex = message.index; maxIndexMessage = message; } }); if (!maxIndexMessage) return []; // Build path backwards through parent_message_uuid const mainPath = []; let currentMessage = maxIndexMessage; while (currentMessage) { mainPath.unshift(currentMessage); const parentUuid = currentMessage.parent_message_uuid; if (parentUuid === "00000000-0000-4000-8000-000000000000" || !parentUuid) { break; } currentMessage = tree.messageMap.get(parentUuid); } return mainPath; } /** * Gets all branch information including branch points */ function getAllBranchInfo(tree) { const branches = []; let branchCounter = 0; // Find main path from MAX INDEX (for determining Main/Side) const mainBranchPath = findMainBranchPathFromMaxIndex(tree); const mainBranchUuids = new Set(mainBranchPath.map(msg => msg.uuid)); function traverseBranches(node, currentPath = [], branchStartIndex = 0) { const newPath = [...currentPath, node]; if (node.children.length === 0) { // Leaf node - this is a complete branch branchCounter++; // Check if this branch is main const isMainBranch = newPath.every(msg => mainBranchUuids.has(msg.uuid)) && newPath.length === mainBranchPath.length; branches.push({ branchId: node.uuid, branchIndex: branchCounter, fullPath: newPath, branchStartIndex: branchStartIndex, isMainBranch: isMainBranch }); } else if (node.children.length === 1) { // Single child - continue same branch traverseBranches(node.children[0], newPath, branchStartIndex); } else { // Multiple children - branch point node.children.forEach((child, childIndex) => { const newBranchStart = childIndex === 0 ? branchStartIndex : newPath.length; traverseBranches(child, newPath, newBranchStart); }); } } tree.rootMessages.forEach(root => { traverseBranches(root, [], 0); }); return { branches, mainBranchUuids: mainBranchUuids // Based on MAX INDEX }; } /** * Creates a Map of messageUuid to branch info */ function createMessageBranchMap(branches) { const messageBranchMap = new Map(); branches.forEach(branch => { branch.fullPath.forEach(msg => { if (!messageBranchMap.has(msg.uuid)) { messageBranchMap.set(msg.uuid, { branchIndex: branch.branchIndex, isMainBranch: branch.isMainBranch }); } }); }); return messageBranchMap; } // ============================================= // DOM MANIPULATION // ============================================= /** * Formats timestamp for display */ function formatTimestamp(isoString) { const date = new Date(isoString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } /** * Injects metadata into a DOM element */ function injectMetadata(container, messageData, mainBranchUuids, messageBranchMap) { // Check if already added if (container.querySelector('.claude-timestamp-metadata')) { return; } // Determine if main or side branch BY SPECIFIC MESSAGE UUID // Not by branch info, because a branch can contain both main and side messages const isMain = mainBranchUuids.has(messageData.uuid); const branchStatus = isMain ? 'Main' : 'Side'; // Get branch number const branchInfo = messageBranchMap.get(messageData.uuid); const branchNumber = branchInfo ? branchInfo.branchIndex : '?'; // Create metadata element const metadata = document.createElement('div'); metadata.className = 'claude-timestamp-metadata'; metadata.style.cssText = ` position: absolute; top: -15px; right: 8px; font-size: 10px; color: var(--text-400, #94a3b8); opacity: 0.7; padding: 2px 6px; background: var(--bg-200, rgba(0, 0, 0, 0.05)); border-radius: 4px; /* backdrop-filter: blur(4px); */ z-index: -1; pointer-events: none; user-select: none; white-space: nowrap; `; const timestamp = formatTimestamp(messageData.created_at); metadata.textContent = `#${messageData.index} | Branch ${branchNumber} (${branchStatus}) | ${timestamp}`; metadata.title = `Index: ${messageData.index}\nBranch: ${branchNumber} (${branchStatus})\nCreated: ${new Date(messageData.created_at).toLocaleString()}\nUUID: ${messageData.uuid}`; // Find where to insert const groupDiv = container.querySelector('.group.relative'); if (groupDiv) { groupDiv.style.position = 'relative'; groupDiv.appendChild(metadata); } } /** * Main function to inject timestamps into all messages */ async function injectTimestamps() { try { // Get conversation data from API const conversationData = await getConversationData(); if (!conversationData.chat_messages || conversationData.chat_messages.length === 0) { console.log('[Claude Timestamps] No messages found'); return; } // Build conversation tree const tree = buildConversationTree(conversationData.chat_messages); // Get all branch information (uses MAX INDEX for Main/Side determination) const { branches, mainBranchUuids } = getAllBranchInfo(tree); // Create message to branch mapping const messageBranchMap = createMessageBranchMap(branches); // Find active branch from current_leaf_message_uuid (for DOM matching) const activeBranch = findMainBranchPathFromLeaf(tree, conversationData.current_leaf_message_uuid); if (activeBranch.length === 0) { console.log('[Claude Timestamps] No active branch found'); return; } // Get DOM elements const messageContainers = document.querySelectorAll( '.flex-1.flex-col.gap-3 > div[data-test-render-count]' ); if (messageContainers.length === 0) { console.log('[Claude Timestamps] No message containers found in DOM'); return; } // Match and inject activeBranch.forEach((msg, index) => { if (index < messageContainers.length) { injectMetadata(messageContainers[index], msg, mainBranchUuids, messageBranchMap); } }); console.log(`[Claude Timestamps] Injected timestamps for ${Math.min(activeBranch.length, messageContainers.length)} messages`); console.log(`[Claude Timestamps] Total branches found: ${branches.length}`); console.log(`[Claude Timestamps] Main branch UUIDs (from max index): ${mainBranchUuids.size} messages`); console.log(`[Claude Timestamps] Active branch (from current_leaf): ${activeBranch.length} messages`); } catch (error) { console.error('[Claude Timestamps] Error:', error); } } // ============================================= // INITIALIZATION & OBSERVERS // ============================================= /** * Debounce function to limit execution rate */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Initialize the script */ function init() { console.log('[Claude Timestamps] Initializing...'); // Initial injection setTimeout(injectTimestamps, 1000); // Watch for DOM changes const observer = new MutationObserver(debounce(() => { injectTimestamps(); }, 500)); // Observe the messages container const messagesContainer = document.querySelector('.flex-1.flex-col.gap-3'); if (messagesContainer) { observer.observe(messagesContainer, { childList: true, subtree: true }); } // Re-inject on URL changes (navigation between chats) let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; console.log('[Claude Timestamps] URL changed, re-injecting...'); setTimeout(injectTimestamps, 1000); } }).observe(document, { subtree: true, childList: true }); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();