Displays Date & Times within Anthropic Claude Conversations (ES5-safe)

Reveal hidden timestamps in Claude conversations with robust DOM/API handling, XHR support, and debounced observer; always show year; larger font.

当前为 2025-08-14 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Displays Date & Times within Anthropic Claude Conversations (ES5-safe)
// @namespace    http://tampermonkey.net/
// @version      2.6
// @license      MIT
// @description  Reveal hidden timestamps in Claude conversations with robust DOM/API handling, XHR support, and debounced observer; always show year; larger font.
// @author       Wayne
// @match        https://claude.ai/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  var CONFIG = {
    timestampClass: 'claude-timestamp',
    messageSelector: "[data-testid*='message'], [class*='message'], .font-claude-message, [role='article'], div[class*='group']",
    observerDelay: 500,
    debounceDelay: 250,
    timestampFormat: 'absolute',
    position: 'inline',
    maxJsonScriptSize: 500000, // Increased limit
    timestampFontSizePx: 15,
    debugMode: true // Enable debugging
  };

  function log(message, data) {
    if (CONFIG.debugMode) {
      console.log('[Claude Timestamps]', message, data || '');
    }
  }

  function formatTimestamp(isoString) {
    var date = new Date(isoString);
    if (isNaN(date.getTime())) {
      log('Invalid timestamp:', isoString);
      return null;
    }
    return date.toLocaleString('en-US', {
      month: 'short',
      day: 'numeric',
      hour: 'numeric',
      minute: '2-digit',
      hour12: true,
      year: 'numeric'
    });
  }

  function injectStyles() {
    if (document.getElementById('claude-timestamp-styles')) return;
    var style = document.createElement('style');
    style.id = 'claude-timestamp-styles';
    style.textContent =
      '.' + CONFIG.timestampClass + ' {' +
      'font-size: ' + CONFIG.timestampFontSizePx + 'px;' +
      'color: #5d6269;' +
      'margin-bottom: 6px;' +
      "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;" +
      'display: block;' +
      'opacity: 0.7;' +
      '}' +
      '@media (prefers-color-scheme: dark) {' +
      '.' + CONFIG.timestampClass + ' { color: #a2acba; }' +
      '}';
    document.head.appendChild(style);
    log('Styles injected');
  }

  function debugMessageElement(element) {
    if (!CONFIG.debugMode) return;
    
    log('Debugging message element:', element);
    log('Element classes:', element.className);
    log('Element attributes:', Array.from(element.attributes || []).map(function(a) {
      return a.name + '=' + a.value;
    }));
    
    // Check for React properties
    var reactKeys = Object.keys(element).filter(function(k) {
      return k.includes('react') || k.includes('React') || k.includes('fiber') || k.includes('Fiber');
    });
    log('React-related keys:', reactKeys);
    
    // Check parent elements for data
    var parent = element.parentElement;
    if (parent) {
      log('Parent classes:', parent.className);
      log('Parent attributes:', Array.from(parent.attributes || []).map(function(a) {
        return a.name + '=' + a.value;
      }));
    }
  }

  function extractTimestampFromAllScripts() {
    var allScripts = document.querySelectorAll('script[type="application/json"], script:not([src])');
    log('Found scripts:', allScripts.length);
    
    for (var i = 0; i < allScripts.length; i++) {
      var script = allScripts[i];
      var content = script.textContent || script.innerHTML || '';
      
      if (content.length > CONFIG.maxJsonScriptSize) {
        log('Skipping large script:', content.length + ' chars');
        continue;
      }
      
      if (content && (content.indexOf('created_at') > -1 || content.indexOf('timestamp') > -1 || content.indexOf('message') > -1)) {
        try {
          var data = JSON.parse(content);
          log('Parsed script data keys:', Object.keys(data));
          
          // Look for timestamps in various structures
          if (data.created_at) return data.created_at;
          if (data.timestamp) return data.timestamp;
          if (data.messages && Array.isArray(data.messages) && data.messages.length > 0) {
            var msg = data.messages[0];
            if (msg.created_at) return msg.created_at;
            if (msg.timestamp) return msg.timestamp;
          }
          if (data.conversation && data.conversation.created_at) return data.conversation.created_at;
        } catch (e) {
          // Not JSON or malformed
        }
      }
    }
    return null;
  }

  function extractTimestamp(messageElement) {
    debugMessageElement(messageElement);
    
    // Method 1: Direct data attributes
    var dataAttrs = ['data-timestamp', 'data-created-at', 'data-time', 'data-message-time'];
    for (var i = 0; i < dataAttrs.length; i++) {
      var value = messageElement.getAttribute(dataAttrs[i]);
      if (value) {
        log('Found timestamp in data attribute:', value);
        return value;
      }
    }

    // Method 2: Check parent elements for data attributes
    var current = messageElement;
    for (var depth = 0; depth < 5 && current; depth++) {
      for (var j = 0; j < dataAttrs.length; j++) {
        var value = current.getAttribute(dataAttrs[j]);
        if (value) {
          log('Found timestamp in parent data attribute:', value);
          return value;
        }
      }
      current = current.parentElement;
    }

    // Method 3: Look for time elements
    var timeElement = messageElement.querySelector('time[datetime]') || 
                     messageElement.parentElement?.querySelector('time[datetime]');
    if (timeElement) {
      var datetime = timeElement.getAttribute('datetime');
      if (datetime) {
        log('Found timestamp in time element:', datetime);
        return datetime;
      }
    }

    // Method 4: Search all JSON scripts
    var scriptTimestamp = extractTimestampFromAllScripts();
    if (scriptTimestamp) {
      log('Found timestamp in script:', scriptTimestamp);
      return scriptTimestamp;
    }

    // Method 5: Network intercepted data
    if (window.conversationData && window.conversationData.messages) {
      var messageId = messageElement.id || messageElement.getAttribute('data-message-id') || 
                      messageElement.getAttribute('data-testid');
      
      for (var m = 0; m < window.conversationData.messages.length; m++) {
        var msg = window.conversationData.messages[m];
        if (msg.id === messageId || (messageId && msg.id && msg.id.indexOf(messageId) > -1)) {
          if (msg.created_at) {
            log('Found timestamp in intercepted data:', msg.created_at);
            return msg.created_at;
          }
        }
      }
      
      // Fallback: use first message timestamp for all messages
      if (window.conversationData.messages[0] && window.conversationData.messages[0].created_at) {
        log('Using fallback timestamp from first message');
        return window.conversationData.messages[0].created_at;
      }
    }

    // Method 6: React Fiber (updated for modern React)
    var keys = Object.keys(messageElement);
    for (var k = 0; k < keys.length; k++) {
      var key = keys[k];
      if (key.indexOf('__reactFiber') === 0 || key.indexOf('__reactInternalInstance') === 0) {
        try {
          var fiber = messageElement[key];
          // Navigate through fiber tree to find props
          var current = fiber;
          for (var attempts = 0; attempts < 10 && current; attempts++) {
            if (current.memoizedProps) {
              var props = current.memoizedProps;
              if (props.message && props.message.created_at) {
                log('Found timestamp in React fiber:', props.message.created_at);
                return props.message.created_at;
              }
              if (props.created_at) {
                log('Found timestamp in React props:', props.created_at);
                return props.created_at;
              }
              if (props.timestamp) {
                log('Found timestamp in React props:', props.timestamp);
                return props.timestamp;
              }
            }
            current = current.return || current.parent;
          }
        } catch (e) {
          log('Error accessing React fiber:', e.message);
        }
        break;
      }
    }

    log('No timestamp found for element');
    return null;
  }

  function addTimestamp(messageElement, timestamp) {
    if (messageElement.querySelector('.' + CONFIG.timestampClass)) return;

    var formatted = formatTimestamp(timestamp);
    if (!formatted) return;

    var timestampElement = document.createElement('div');
    timestampElement.className = CONFIG.timestampClass;
    timestampElement.textContent = formatted;

    // Insert at the beginning of the message
    if (messageElement.firstChild) {
      messageElement.insertBefore(timestampElement, messageElement.firstChild);
    } else {
      messageElement.appendChild(timestampElement);
    }
    
    log('Added timestamp to message:', formatted);
  }

  function processMessages() {
    log('Processing messages...');
    var messages = document.querySelectorAll(CONFIG.messageSelector);
    log('Found message elements:', messages.length);
    
    var timestampsAdded = 0;
    for (var i = 0; i < messages.length; i++) {
      var messageElement = messages[i];
      
      // Skip if already has timestamp
      if (messageElement.querySelector('.' + CONFIG.timestampClass)) continue;
      
      var timestamp = extractTimestamp(messageElement);
      if (timestamp) {
        addTimestamp(messageElement, timestamp);
        timestampsAdded++;
      }
    }
    
    log('Timestamps added:', timestampsAdded);
  }

  function interceptNetworkData() {
    if (!window.fetch) return;
    
    var originalFetch = window.fetch;
    window.fetch = function () {
      var args = arguments;
      var req = args[0];
      var url = typeof req === 'string' ? req : (req && req.url ? req.url : '');
      
      return originalFetch.apply(this, args).then(function (res) {
        if (url && (url.indexOf('/conversations') > -1 || url.indexOf('/chat') > -1 || url.indexOf('/api') > -1)) {
          log('Intercepted fetch to:', url);
          try {
            var clone = res.clone();
            var ct = '';
            try {
              ct = clone.headers.get('content-type') || '';
            } catch (e) {}
            
            if (ct.indexOf('application/json') > -1) {
              clone.json().then(function (data) {
                if (data && (data.messages || data.conversation || data.chat)) {
                  log('Stored conversation data from fetch');
                  window.conversationData = data;
                  setTimeout(processMessages, 200);
                }
              })["catch"](function (e) {
                log('Error parsing JSON from fetch:', e.message);
              });
            }
          } catch (e) {
            log('Error processing fetch response:', e.message);
          }
        }
        return res;
      });
    };
  }

  function interceptXHR() {
    var originalOpen = XMLHttpRequest.prototype.open;
    var originalSend = XMLHttpRequest.prototype.send;
    
    XMLHttpRequest.prototype.open = function (method, url) {
      this._interceptedUrl = url;
      return originalOpen.apply(this, arguments);
    };
    
    XMLHttpRequest.prototype.send = function () {
      var xhr = this;
      var originalOnLoad = xhr.onload;
      
      xhr.addEventListener('load', function () {
        if (xhr._interceptedUrl && 
            (xhr._interceptedUrl.indexOf('/conversations') > -1 || 
             xhr._interceptedUrl.indexOf('/chat') > -1 || 
             xhr._interceptedUrl.indexOf('/api') > -1)) {
          log('Intercepted XHR to:', xhr._interceptedUrl);
          try {
            var ct = xhr.getResponseHeader('content-type') || '';
            if (ct.indexOf('application/json') > -1) {
              var data = JSON.parse(xhr.responseText);
              if (data && (data.messages || data.conversation || data.chat)) {
                log('Stored conversation data from XHR');
                window.conversationData = data;
                setTimeout(processMessages, 200);
              }
            }
          } catch (e) {
            log('Error processing XHR response:', e.message);
          }
        }
        if (originalOnLoad) originalOnLoad.call(xhr);
      });
      
      return originalSend.apply(this, arguments);
    };
  }

  function debounce(fn, delay) {
    var timer;
    return function () {
      var context = this, args = arguments;
      clearTimeout(timer);
      timer = setTimeout(function () {
        fn.apply(context, args);
      }, delay);
    };
  }

  function setupObserver() {
    var debouncedProcess = debounce(processMessages, CONFIG.debounceDelay);
    var observer = new MutationObserver(function (mutations) {
      var shouldProcess = false;
      for (var i = 0; i < mutations.length; i++) {
        var mutation = mutations[i];
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
          for (var n = 0; n < mutation.addedNodes.length; n++) {
            var node = mutation.addedNodes[n];
            if (node.nodeType === 1) {
              // Check if node is a message or contains messages
              if ((node.matches && node.matches(CONFIG.messageSelector)) ||
                  (node.querySelector && node.querySelector(CONFIG.messageSelector))) {
                shouldProcess = true;
                break;
              }
            }
          }
        }
        if (shouldProcess) break;
      }
      if (shouldProcess) {
        log('DOM mutation detected, processing messages');
        debouncedProcess();
      }
    });
    
    observer.observe(document.body, { 
      childList: true, 
      subtree: true,
      attributes: false, // Don't watch attributes to reduce noise
      characterData: false 
    });
    
    log('DOM observer setup complete');
    return observer;
  }

  function init() {
    log('Initializing Claude timestamp script');
    injectStyles();
    interceptNetworkData();
    interceptXHR();
    
    // Initial processing with increasing delays
    setTimeout(processMessages, 1000);
    setTimeout(processMessages, 3000);
    setTimeout(processMessages, 5000);
    
    setupObserver();
    
    // Periodic processing for missed updates
    setInterval(processMessages, 15000);
    
    log('Initialization complete');
  }

  // Start when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    setTimeout(init, 100);
  }
})();