// ==UserScript== // @name Claude 对话导航 // @namespace http://tampermonkey.net/ // @version 1.0 // @description Claude官网对话导航工具:紧凑导航 + 实时定位;快捷键 Command+↑/↓ 与 Alt+[ / Alt+];回到顶部/到底部单击即用 // @author schweigen // @license MIT // @match https://claude.ai/* // @match https://claude.ai/chat/* // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @downloadURL none // ==/UserScript== (function () { 'use strict'; const CONFIG = { maxPreviewLength: 12, animation: 250, refreshInterval: 2000, forceRefreshInterval: 10000, anchorOffset: 8 }; const BOUNDARY_EPS = 28; const DEBUG = false; // 全局调试函数,用户可在控制台调用 window.claudeNavDebug = { forceRefresh: () => { console.log('Claude Navigation: 手动强制刷新'); TURN_SELECTOR = null; const ui = document.getElementById('claude-compact-nav')?._ui; if (ui) scheduleRefresh(ui); else console.log('导航面板未找到'); }, showCurrentSelector: () => { console.log('当前使用的选择器:', TURN_SELECTOR || '无'); console.log('当前对话数量:', qsTurns().length); }, testAllSelectors: () => { const originalSelector = TURN_SELECTOR; TURN_SELECTOR = null; qsTurns(); // 这会触发调试输出 TURN_SELECTOR = originalSelector; }, getCurrentTurns: () => { const turns = qsTurns(); console.log('当前检测到的对话元素:', turns); return turns; }, checkOverlap: () => { const panels = document.querySelectorAll('#claude-compact-nav'); const styles = document.querySelectorAll('#claude-compact-nav-style'); console.log(`找到 ${panels.length} 个导航面板`); console.log(`找到 ${styles.length} 个样式节点`); console.log(`键盘事件已绑定: ${!!window.__claudeKeysBound}`); console.log(`正在启动中: ${__claudeBooting}`); if (panels.length > 1) { console.warn('检测到重叠面板!清理中...'); panels.forEach((panel, index) => { if (index > 0) { panel.remove(); console.log(`已删除重复面板 ${index}`); } }); } return { panels: panels.length, styles: styles.length, keysBound: !!window.__claudeKeysBound, booting: __claudeBooting }; } }; GM_registerMenuCommand("重置问题栏位置", resetPanelPosition); function resetPanelPosition() { const nav = document.getElementById('claude-compact-nav'); if (nav) { nav.style.top = '60px'; nav.style.right = '10px'; nav.style.left = 'auto'; nav.style.bottom = 'auto'; const originalBg = nav.style.background; nav.style.background = 'rgba(0, 255, 0, 0.2)'; setTimeout(() => { nav.style.background = originalBg; }, 500); } } let pending = false, rafId = null, idleId = null; let forceRefreshTimer = null; let lastTurnCount = 0; let TURN_SELECTOR = null; let scrollTicking = false; let currentActiveId = null; let __claudeBooting = false; let refreshTimer = 0; function scheduleRefresh(ui, { delay = 80, force = false } = {}) { if (force) { if (refreshTimer) { clearTimeout(refreshTimer); refreshTimer = 0; } run(); return; } if (refreshTimer) clearTimeout(refreshTimer); refreshTimer = setTimeout(run, delay); function run() { refreshTimer = 0; pending = false; try { const oldCount = cacheIndex.length; refreshIndex(ui); const newCount = cacheIndex.length; if (newCount !== oldCount) { setTimeout(() => { refreshIndex(ui); scheduleActiveUpdateNow(); }, 120); } else { scheduleActiveUpdateNow(); } } catch (e) { if (DEBUG || window.DEBUG_TEMP) console.error('scheduleRefresh error:', e); } } } function init() { if (document.getElementById('claude-compact-nav')) return; const checkContentLoaded = () => { const turns = document.querySelectorAll('[data-testid="user-message"], .font-claude-response, [data-test-render-count]'); return turns.length > 0; }; const boot = () => { if (document.getElementById('claude-compact-nav')) { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 面板已存在,跳过创建'); return; } if (__claudeBooting) { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 正在启动中,跳过重复创建'); return; } __claudeBooting = true; try { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 开始创建面板'); const ui = createPanel(); wirePanel(ui); observeChat(ui); bindActiveTracking(); watchSendEvents(ui); scheduleRefresh(ui); if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 面板创建完成'); } finally { __claudeBooting = false; } }; if (checkContentLoaded()) boot(); else { const observer = new MutationObserver(() => { if (checkContentLoaded()) { observer.disconnect(); boot(); } }); observer.observe(document.body, { childList: true, subtree: true }); } } let currentUrl = location.href; function detectUrlChange() { if (location.href !== currentUrl) { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: URL变化,清理旧实例', currentUrl, '->', location.href); currentUrl = location.href; const oldNav = document.getElementById('claude-compact-nav'); if (oldNav) { if (oldNav._ui) { if (oldNav._ui._forceRefreshTimer) { clearInterval(oldNav._ui._forceRefreshTimer); if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 已清理定时器'); } if (oldNav._ui._mo) { try { oldNav._ui._mo.disconnect(); if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 已断开MutationObserver'); } catch (e) { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 断开MutationObserver失败', e); } } } oldNav.remove(); if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 已移除旧面板'); } __claudeBooting = false; window.__claudeKeysBound = false; lastTurnCount = 0; TURN_SELECTOR = null; setTimeout(init, 100); } } window.addEventListener('popstate', detectUrlChange); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (...args) { originalPushState.apply(this, args); setTimeout(detectUrlChange, 0); }; history.replaceState = function (...args) { originalReplaceState.apply(this, args); setTimeout(detectUrlChange, 0); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); function qsTurns(root = document) { if (TURN_SELECTOR) return Array.from(root.querySelectorAll(TURN_SELECTOR)); // Claude 特定的选择器 const selectors = [ // 主要对话容器选择器 '.flex-1.flex.flex-col.gap-3 > div', '.flex-1 > div[data-test-render-count]', 'div[data-test-render-count]', // 备用选择器 '[data-testid="user-message"]', '.font-claude-response', 'div[class*="group"][class*="relative"]', '.flex.flex-col.gap-3 > div', 'main div[class*="group"]', // 更广泛的备用选择器 'div[class*="mb-1"][class*="mt-1"]', 'div[style*="opacity: 1; transform: none"]', '.max-w-3xl div[class*="group"]' ]; if (DEBUG || window.DEBUG_TEMP) { console.log('Claude Navigation Debug: 检测对话选择器'); for (const selector of selectors) { const els = root.querySelectorAll(selector); console.log(`- ${selector}: ${els.length} 个元素`); if (els.length > 0) { console.log(' 样本元素:', els[0]); } } } for (const selector of selectors) { const els = root.querySelectorAll(selector); // 过滤出包含对话内容的元素 const validEls = Array.from(els).filter(el => { return el.querySelector('[data-testid="user-message"]') || el.querySelector('.font-claude-response') || el.querySelector('.whitespace-pre-wrap') || (el.textContent && el.textContent.trim().length > 20); }); if (validEls.length) { TURN_SELECTOR = selector; if (DEBUG || window.DEBUG_TEMP) console.log(`Claude Navigation: 使用选择器 ${selector}, 找到 ${validEls.length} 个对话`); return validEls; } } if (DEBUG || window.DEBUG_TEMP) { console.log('Claude Navigation Debug: 所有预设选择器都失效,尝试智能检测'); } // 智能检测 fallback const fallbackSelectors = [ 'div[class*="group"], div[class*="relative"]', 'div[class*="mb-1"], div[class*="mt-1"]', '.max-w-3xl > div > div', 'main > div > div' ]; for (const fallbackSelector of fallbackSelectors) { const candidates = [...root.querySelectorAll(fallbackSelector)].filter(el => { return ( el.querySelector('[data-testid="user-message"]') || el.querySelector('.font-claude-response') || el.querySelector('.whitespace-pre-wrap') || (el.textContent && el.textContent.trim().length > 20 && !el.querySelector('button') && !el.querySelector('input')) ); }); if (candidates.length > 0) { if (DEBUG || window.DEBUG_TEMP) console.log(`Claude Navigation: Fallback选择器 ${fallbackSelector} 找到 ${candidates.length} 个候选对话`); return candidates; } } if (DEBUG) console.log('Claude Navigation: 所有检测方法均失效'); return []; } function getTextPreview(el) { if (!el) return ''; const text = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim(); if (!text) return '...'; let width = 0, result = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; const charWidth = /[\u4e00-\u9fa5]/.test(char) ? 2 : 1; if (width + charWidth > CONFIG.maxPreviewLength) { result += '…'; break; } result += char; width += charWidth; } return result || text.slice(0, CONFIG.maxPreviewLength) || '...'; } function buildIndex() { const turns = qsTurns(); if (!turns.length) { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 没有找到任何对话元素'); return []; } if (DEBUG) console.log(`Claude Navigation: 开始分析 ${turns.length} 个对话元素`); let u = 0, a = 0; const list = []; for (let i = 0; i < turns.length; i++) { const el = turns[i]; el.setAttribute('data-claude-turn', '1'); // 检测是否为用户消息 const isUser = !!( el.querySelector('[data-testid="user-message"]') || el.querySelector('.text-text-100') || el.querySelector('div[class*="bg-bg-300"]') ); // 检测是否为Claude回复 const isAssistant = !!( el.querySelector('.font-claude-response') || el.querySelector('[data-is-streaming]') || (!isUser && el.querySelector('.whitespace-normal')) ); if (DEBUG && i < 3) { console.log(`Claude Navigation Debug - 元素 ${i}:`, { element: el, isUser, isAssistant, userSelectors: { userMessage: !!el.querySelector('[data-testid="user-message"]'), textColor: !!el.querySelector('.text-text-100'), bgColor: !!el.querySelector('div[class*="bg-bg-300"]') }, assistantSelectors: { claudeResponse: !!el.querySelector('.font-claude-response'), streaming: !!el.querySelector('[data-is-streaming]'), whitespace: !!el.querySelector('.whitespace-normal') } }); } let block = null; if (isUser) { block = el.querySelector('[data-testid="user-message"] .whitespace-pre-wrap, [data-testid="user-message"] p, [data-testid="user-message"]'); } else if (isAssistant) { block = el.querySelector('.font-claude-response .whitespace-normal, .font-claude-response p, .font-claude-response'); } else { if (DEBUG && i < 5) console.log(`Claude Navigation: 元素 ${i} 角色识别失败`); continue; } const preview = getTextPreview(block || el); if (!preview || preview === '...') { if (DEBUG && i < 5) console.log(`Claude Navigation: 元素 ${i} 无法提取预览文本`); continue; } if (!el.id) el.id = `claude-turn-${i + 1}`; const role = isUser ? 'user' : 'assistant'; const seq = isUser ? ++u : ++a; list.push({ id: el.id, idx: i, role, preview, seq }); } if (DEBUG) console.log(`Claude Navigation: 成功识别 ${list.length} 个对话 (用户: ${u}, 助手: ${a})`); return list; } function createPanel() { const styleId = 'claude-compact-nav-style'; let style = document.getElementById(styleId); if (!style) { style = document.createElement('style'); style.id = styleId; style.textContent = ` #claude-compact-nav { position: fixed; top: 60px; right: 10px; width: auto; min-width: 80px; max-width: 210px; z-index: 2147483647 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 13px; pointer-events: auto; background: transparent; -webkit-user-select:none; user-select:none; -webkit-tap-highlight-color: transparent; } #claude-compact-nav * { -webkit-user-select:none; user-select:none; } .compact-header { display:flex; align-items:center; justify-content:space-between; padding:4px 8px; margin-bottom:4px; background:transparent; border-radius:6px; pointer-events:auto; cursor:move; box-shadow:0 1px 3px rgba(0,0,0,.08); min-width:100px; } .compact-title { font-size:11px; font-weight:600; color: rgb(147, 51, 234); display:flex; align-items:center; gap:3px; } .compact-title svg { width:12px; height:12px; opacity:.5; } .compact-toggle { background:rgba(255,255,255,.9); border:1px solid rgba(0,0,0,.15); color:rgba(0,0,0,.6); cursor:pointer; width:26px; height:26px; display:flex; align-items:center; justify-content:center; border-radius:4px; transition:all .2s; font-size:20px; font-weight:bold; line-height:1; } .compact-toggle:hover { background:rgba(0,0,0,.1); color:rgba(0,0,0,.8); border-color:rgba(0,0,0,.25); } .compact-refresh { background:rgba(255,255,255,.9); border:1px solid rgba(0,0,0,.15); color:rgba(0,0,0,.6); cursor:pointer; width:26px; height:26px; display:flex; align-items:center; justify-content:center; border-radius:4px; transition:all .2s; font-size:14px; font-weight:bold; line-height:1; margin-left:4px; } .compact-refresh:hover { background:rgba(0,0,0,.1); color:rgba(0,0,0,.8); border-color:rgba(0,0,0,.25); } .toggle-text { display:block; font-family:monospace; } .compact-list { max-height:400px; overflow-y:auto; overflow-x:hidden; padding:0; pointer-events:auto; display:flex; flex-direction:column; gap:8px; } .compact-list::-webkit-scrollbar { width:3px; } .compact-list::-webkit-scrollbar-thumb { background:rgba(0,0,0,.2); border-radius:2px; } .compact-list::-webkit-scrollbar-thumb:hover { background:rgba(0,0,0,.3); } .compact-item { display:block; padding:3px 8px; margin:0; border-radius:4px; cursor:pointer; transition:all .15s ease; font-size:12px; line-height:1.4; min-height:20px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:auto; background:rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.05); width:auto; min-width:60px; max-width:190px; } .compact-item:hover { background:rgba(255,255,255,.95); transform:translateX(2px); box-shadow:0 2px 4px rgba(0,0,0,.1); } .compact-item.user { color: rgb(139, 69, 19); border-left:2px solid rgba(139,69,19,.7); font-weight: 500; } .compact-item.assistant { color: rgb(25, 25, 112); border-left:2px solid rgba(25,25,112,.7); font-weight: 500; } .compact-item.active { outline:2px solid rgba(147,51,234,.5); background: rgba(147,51,234,.07); } .compact-text { display:inline-block; } .compact-number { display:inline-block; margin-right:4px; font-weight:600; opacity:.7; font-size:11px; } .compact-empty { padding:10px; text-align:center; color:#999; font-size:11px; background:rgba(255,255,255,.85); border-radius:6px; pointer-events:auto; min-height:20px; line-height:1.4; } /* 底部导航条 */ .compact-footer { margin-top:6px; display:flex; gap:6px; } .nav-btn { flex:1 1 auto; padding:6px 8px; font-size:14px; border-radius:6px; border:1px solid rgba(0,0,0,.15); background:rgba(255,255,255,.9); cursor:pointer; box-shadow:0 1px 2px rgba(0,0,0,.05); line-height:1; } .nav-btn:hover { background:rgba(0,0,0,.06); } .nav-btn:active { transform: translateY(1px); } /* 上下箭头为橙色系 */ .nav-btn.arrow { background: rgba(255, 127, 80, 0.25); border-color: rgba(255, 127, 80, 0.35); } .nav-btn.arrow:hover { background: rgba(255, 127, 80, 0.35); } /* 移动端 */ @media (max-width: 768px) { #claude-compact-nav { right:5px; max-width:160px; } .compact-item { font-size:11px; padding:2px 5px; min-height:18px; } .nav-btn { padding:5px 6px; font-size:13px; } } .highlight-pulse { animation: pulse 1.5s ease-out; } @keyframes pulse { 0% { background-color: rgba(255,243,205,0); } 20% { background-color: rgba(255,243,205,1); } 100% { background-color: rgba(255,243,205,0); } } `; document.head.appendChild(style); if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 已创建样式'); } else { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 样式已存在,跳过创建'); } const existingPanels = document.querySelectorAll('#claude-compact-nav'); if (existingPanels.length > 0) { if (DEBUG || window.DEBUG_TEMP) console.log(`Claude Navigation: 发现 ${existingPanels.length} 个已存在的面板,清理中...`); existingPanels.forEach((panel, index) => { if (index > 0) { panel.remove(); if (DEBUG || window.DEBUG_TEMP) console.log(`Claude Navigation: 已删除重复面板 ${index}`); } }); if (existingPanels.length > 0) { const existingNav = existingPanels[0]; if (existingNav._ui) { if (DEBUG || window.DEBUG_TEMP) console.log('Claude Navigation: 返回已存在的面板'); return existingNav._ui; } } } const nav = document.createElement('div'); nav.id = 'claude-compact-nav'; nav.innerHTML = `