// ==UserScript== // @name Claude helper (对话导出\字数统计\时间显示) // @name:zh-CN Claude 助手 (对话导出\字数统计\时间显示) // @version 0.5.8 // @description ✴️1、可以导出 claude ai当前对话的内容。✴️2、统计当前字数 (包括粘贴、上传、article的内容,含换行符/markdown语法符号等)。✴️3、显示对话的时间。✴️4、显示对话的模型信息、Token信息。ℹ️显示的信息均来自网页内本身存在但未显示的属性值。 // @author Yearly // @match https://claude.ai/* // @include https://*claude*.com/* // @match https://chat.kelaode.ai/* // @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGg4MHY4MEgweiIgZmlsbD0iIzQ0NSIvPjxwYXRoIGQ9Im0zMyA0NC0yMy0xYy0xIDAtMi0yLTItM3MwLTEgMS0xbDI0IDItMjEtMTVjMC0xLTEtMS0xLTNzMy00IDYtMmwxNCAxMi05LTE3di0yYzAtMSAxLTUgMy01IDEgMCAzIDAgNCAxbDExIDIzIDItMjBjMC0yIDEtNCAzLTRzMyAxIDMgMmwtMyAyMCAxMi0xNGMxLTEgMy0yIDQtMSAyIDIgMiA0IDEgNkw1MSAzN2gxbDEyLTJjMy0xIDYtMiA3IDAgMSAxIDAgMyAwIDNsLTIxIDVjMTQgMSAxNSAwIDE4IDEgMiAwIDMgMiAzIDMgMCAzLTIgMy0zIDNsLTE5LTQgMTUgMTR2MWwtMiAxYy0xIDAtOS03LTE0LTExbDcgMTFjMSAxIDEgMyAwIDRzLTMgMS0zIDBMNDEgNTBjMCA3LTEgMTMtMiAxOSAwIDEtMSAxLTMgMi0xIDAtMy0xLTItM2wxLTQgMy0xNi0xMCAxMy00IDVoLTFjLTEgMC0yLTEtMi0zbDE0LTE4LTE3IDExaC00cy0xLTIgMC0zbDUtNHoiIGZpbGw9IiNENzUiLz48L3N2Zz4= // @license AGPL-v3.0 // @namespace https://greasyfork.org/zh-CN/scripts/502829-claude-helper // @supportURL https://greasyfork.org/zh-CN/scripts/502829-claude-helper // @homepageURL https://greasyfork.org/zh-CN/scripts/502829-claude-helper // @grant GM_addStyle // @downloadURL none // ==/UserScript== (function() { // model info function conversation_model() { let conversation = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ; if(!conversation) return null; let reactProps = Object.keys(conversation).find(key => key.startsWith('__reactProps$')); if (!reactProps) return null; let conversProps = conversation[reactProps]; if (!conversProps) return null; let model = conversProps.children[1]?.props?.children[0]?.props?.conversation?.model; //claude-3-5-sonnet-20240620 return model; } // tokensSoFar function conversation_tokensSoFar() { let conversation = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ; if(!conversation) return null; let reactProps = Object.keys(conversation).find(key => key.startsWith('__reactProps$')); if (!reactProps) return null; let conversProps = conversation[reactProps]; if (!conversProps) return null; let tokensSoFar = conversProps.children[1]?.props?.children[0]?.props?.conversation?.tokensSoFar; return tokensSoFar; } // msg count var last_uuid = '', last_length = 0; function get_msg_count() { let mainScreen = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ; if(!mainScreen) return; let tx_cnts = 0, tx_sz = 0; let rx_cnts = 0, rx_sz = 0; let fp_cnts = 0, fp_sz = 0, img_cnts = 0; let i = 0; let reactProps = Object.keys(mainScreen).find(key => key.startsWith('__reactProps$')); if (!reactProps) return null; let msgProps = mainScreen[reactProps]; let Msgs = (msgProps.children[0]?.props?.messages); if (Msgs && Msgs.length > 0) { let newest_msgs = Msgs[Msgs.length-1]; let uuid = newest_msgs.uuid; let length = newest_msgs.text.length; if (uuid == last_uuid && length == last_length) { return null; } last_uuid = uuid; last_length = length; } else { return null; } Msgs.forEach(function(msg){ if(msg.sender == "human") { tx_cnts +=1; tx_sz += msg.text.length; for(i = 0; i < msg.attachments.length; i++) { tx_sz += msg.attachments[i].file_size; fp_cnts += 1; fp_sz += msg.attachments[i].file_size;; } img_cnts += msg.files.length; } else if(msg.sender == "assistant") { rx_cnts +=1; rx_sz += msg.text.length; } }); return { tx_cnts: tx_cnts, tx_sz: tx_sz, rx_cnts: rx_cnts, rx_sz: rx_sz, fp_cnts: fp_cnts, fp_sz: fp_sz, img_cnts: img_cnts, }; } function msg_counter_main() { let fieldset = document.querySelector("body > div.flex.min-h-screen.w-full fieldset"); if (fieldset) { let ret = get_msg_count(); if(!ret) return; let count_result = document.querySelector("#claude-msg-counter") if(!count_result) { count_result = document.createElement("pre"); count_result.id = "claude-msg-counter"; count_result.className="border-0.5 relative z-[5] text-text-200 border-accent-pro-100/20 bg-accent-pro-900 rounded-t-xl border-b-0" count_result.style = "font-size:12px; padding: 5px 7px 14px; margin:-12px 0; text-wrap: pretty;"; if (fieldset.querySelector("div.flex.md\\:px-2.flex-col-reverse > div") ){ fieldset.querySelector("div.flex.md\\:px-2.flex-col-reverse > div").remove(); } fieldset.querySelector("div.flex.md\\:px-2.flex-col-reverse").append(count_result); } let all_length = ret.tx_sz + ret.rx_sz ; let file_info = "" let img_file_info = "" if (ret.fp_cnts) file_info = ` (包含${ret.fp_cnts}个上传或粘贴文本,${ret.fp_sz}字)` if (ret.img_cnts) img_file_info = ` (另有${ret.img_cnts}个非文本内容的上传或粘贴,不能计量字数)` const model = conversation_model(); const token = conversation_tokensSoFar(); let model_info = ''; if (model) { model_info = `【模型】${model}。`; } let token_info = ''; if (token) { token_info = `【tokensSoFar】${token}。`; } conversation_tokensSoFar count_result.innerText = `【统计】已发出:${ret.tx_cnts}条,${ret.tx_sz}字${file_info}; 已回复:${ret.rx_cnts}条,${ret.rx_sz}字; 总计:${all_length}字${img_file_info}。${model_info}${token_info}`; } } setInterval(() => { msg_counter_main(); }, 1600); // show update time function show_msg_time() { let mainScreen = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ; if(!mainScreen) return; const msg_divs = mainScreen.querySelectorAll("div[data-test-render-count] > div.mb-1.mt-1, div[data-test-render-count] > div > div[data-is-streaming].group"); msg_divs.forEach(function(msg_div){ if (msg_div.nextSibling) return; let reactProps = Object.keys(msg_div).find(key => key.startsWith('__reactProps$')); if (!reactProps) return; let divProps = msg_div[reactProps]; let updated_at = divProps.children?.[1]?.props?.message?.updated_at ?? divProps.children?.[1]?.props?.children?.[2]?.props?.message?.updated_at; //let created_at = divProps.children?.[1]?.props?.message?.created_at ?? divProps.children?.[1]?.props?.children?.[2]?.props?.message?.created_at; if (!updated_at) return; const date = new Date(updated_at); if (!date) return; const localDateStr = date.toLocaleString(); let timeNode = document.createElement("div"); timeNode.innerText = localDateStr; timeNode.className = 'msg-uptime'; //console.log(updated_at, created_at); msg_div.after(timeNode); }); } GM_addStyle(` div[data-test-render-count] > div > .msg-uptime { margin: 1px 5px 5px; font-size: 13px; font-weight: 300; } div[data-test-render-count] > .msg-uptime { margin: -2px 5px 5px; font-size: 13px; font-weight: 300; } `); setInterval(() => { show_msg_time(); }, 2100); // Add Download Button function createPersistentElement(selector, createElementCallback) { function ensureElement() { const targetElement = document.querySelector(selector); if (targetElement) { if (!targetElement.querySelector('.-added-element')) { const newElement = createElementCallback(); newElement.classList.add('-added-element'); targetElement.appendChild(newElement); } } } ensureElement(); const observer = new MutationObserver(() => { ensureElement(); }); observer.observe(document.body, { childList: true, subtree: true }); } function get_msg_context() { let context = ""; let mainScreen = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ; if(!mainScreen) return; let tx_cnts = 0, tx_sz = 0; let rx_cnts = 0, rx_sz = 0; let fp_cnts = 0, fp_sz = 0; let i = 0; let reactProps = Object.keys(mainScreen).find(key => key.startsWith('__reactProps$')); if (!reactProps) return null; let msgProps = mainScreen[reactProps]; let convID = (msgProps.children[0]?.props?.conversationUUID); let name = (msgProps.children[0]?.props?.name); let Msgs = (msgProps.children[0]?.props?.messages); if ( !convID || !name || !Msgs && !Msgs.length <= 0) { return null; } const model = conversation_model(); const token = conversation_tokensSoFar(); let model_info = ''; if (model) { model_info = `model: ${model}\n`; } let token_info = ''; if (token) { token_info = `tokensSoFar: ${token}\n`; } context += `# ${name}\n${model_info}${token_info}conversationUUID: ${convID}\n`; Msgs.forEach(function(msg){ context += `\n## ${msg.sender}:\n\n` context += msg.text + '\n' for(i = 0; i < msg.attachments.length; i++) { context += `file: ${msg.attachments[i].file_name}\n` if(msg.attachments[i].extracted_content) { context += `file_context: ${msg.attachments[i].extracted_content}\n`; } } for(i = 0; i < msg.files.length; i++) { context += `file: ${msg.files[i].file_name}\n` if(msg.files[i].preview_url) { context += `preview_url: ${window.location.origin + msg.files[i].preview_url}\n`; } } context += `\n------------------------------------------------------\n` }); let blob = new Blob([context], {type: 'text/plain;charset=utf-8'}); let fileUrl = URL.createObjectURL(blob); let tempLink = document.createElement('a'); tempLink.href = fileUrl; let fileTitle = name.replaceAll(' ','_') + ".ClaudeAI.export.md"; tempLink.setAttribute('download', fileTitle); tempLink.style.display = 'none'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); URL.revokeObjectURL(fileUrl); return; } function createDownloadButton() { const button = document.createElement("button"); button.className = "inline-flex items-center justify-center relative shrink-0 ring-offset-2 ring-offset-bg-300 ring-accent-main-100 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none text-text-200 transition-all font-styrene active:bg-bg-400 hover:bg-bg-500/40 hover:text-text-100 h-9 w-9 rounded-md active:scale-95 shrink-0"; button.innerHTML = ``; button.title="Download Conversation" button.addEventListener("click", () => { get_msg_context(); }); return button; } // 添加按钮 createPersistentElement("body > div.flex.min-h-screen.w-full div.sticky.items-center div.right-3 div.hidden.flex-row-reverse", createDownloadButton); })();