Skip to content

Instantly share code, notes, and snippets.

@camillanapoles
Last active January 26, 2026 13:16
Show Gist options
  • Select an option

  • Save camillanapoles/30d4066eb7bb37f8ece19b661b7ba24e to your computer and use it in GitHub Desktop.

Select an option

Save camillanapoles/30d4066eb7bb37f8ece19b661b7ba24e to your computer and use it in GitHub Desktop.
Claude Extractor
// ==UserScript==
// @name Claude Extractor Diagnostic
// @namespace https://github.com/cnmfs/claude-extractor
// @version 2.1.0-diag
// @description Versão de diagnóstico para identificar seletores DOM do Claude.ai
// @author cnmfs
// @match https://claude.ai/*
// @icon https://claude.ai/favicon.ico
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const VERSION = '2.1.0-diag';
// ═══════════════════════════════════════════════════════════
// SELETORES A TESTAR
// ═══════════════════════════════════════════════════════════
const SELECTORS_TO_TEST = {
// CONTAINER - Onde estão as mensagens
container: [
'[data-testid="conversation-turn-list"]',
'[data-testid="conversation-messages"]',
'[role="main"]',
'main [class*="flex-col"]',
'[class*="react-scroll-to-bottom"]',
'[class*="overflow-y-auto"]',
'main > div > div',
'main'
],
// MESSAGE GROUP - Cada mensagem individual
messageGroup: [
'[data-testid^="conversation-turn"]',
'[data-testid*="turn"]',
'[class*="group/conversation-turn"]',
'[class*="conversation-turn"]',
'div[class*="group"][class*="relative"]',
'div[class*="min-h-"]:has([class*="prose"])',
'div.group:has([class*="prose"])',
'[role="article"]'
],
// HUMAN - Mensagem do usuário
human: [
'[data-testid="user-message"]',
'[data-testid="human-turn"]',
'[data-testid*="human"]',
'[data-testid*="user"]',
'[aria-label*="you"]',
'[class*="human"]',
'[class*="user-message"]'
],
// ASSISTANT - Mensagem do Claude
assistant: [
'[data-testid="assistant-message"]',
'[data-testid="assistant-turn"]',
'[data-testid*="assistant"]',
'[aria-label*="Claude"]',
'[class*="assistant"]',
'[class*="ai-message"]'
],
// CONTENT - Conteúdo da mensagem
content: [
'[class*="prose"]',
'[class*="markdown"]',
'[class*="font-claude-message"]',
'[class*="message-content"]',
'.whitespace-pre-wrap',
'[data-testid*="content"]'
],
// THINKING - Blocos de pensamento
thinking: [
'[data-testid="thinking-block"]',
'[data-testid="thinking-summary"]',
'[data-testid="extended-thinking"]',
'[data-testid*="thinking"]',
'details[class*="thinking"]',
'[class*="thinking-container"]',
'[class*="thinking"]',
'details:has(summary)'
],
// ARTIFACT - Código/artefatos
artifact: [
'[data-testid="artifact"]',
'[data-artifact-id]',
'[class*="artifact-container"]',
'[class*="artifact"]',
'pre:has(code)',
'pre code'
],
// TOOLS - Chamadas de ferramenta
tools: [
'[data-testid="tool-use"]',
'[data-testid="tool-call"]',
'[data-testid*="tool"]',
'[class*="tool-use"]',
'[class*="function-call"]'
],
// CONSOLE - Output de código
console: [
'[data-testid="code-output"]',
'[data-testid="execution-result"]',
'[class*="code-output"]',
'[class*="execution-result"]',
'[class*="console-output"]',
'pre[class*="output"]',
'[class*="stdout"]'
]
};
// ═══════════════════════════════════════════════════════════
// DIAGNÓSTICO
// ═══════════════════════════════════════════════════════════
const Diagnostic = {
run() {
console.log('═══════════════════════════════════════════════');
console.log('🔍 CLAUDE EXTRACTOR DIAGNOSTIC v' + VERSION);
console.log('═══════════════════════════════════════════════');
console.log('URL:', location.href);
console.log('Timestamp:', new Date().toISOString());
console.log('');
const report = {
version: VERSION,
url: location.href,
timestamp: new Date().toISOString(),
results: {},
workingSelectors: {},
recommendations: []
};
// Testar cada categoria de seletores
for (const [category, selectors] of Object.entries(SELECTORS_TO_TEST)) {
console.log(`\n📋 Testando: ${category.toUpperCase()}`);
report.results[category] = [];
report.workingSelectors[category] = [];
for (const sel of selectors) {
try {
const elements = document.querySelectorAll(sel);
const count = elements.length;
const status = count > 0 ? '✅' : '❌';
console.log(` ${status} ${sel} → ${count} encontrado(s)`);
report.results[category].push({
selector: sel,
count,
status: count > 0 ? 'found' : 'not_found'
});
if (count > 0) {
report.workingSelectors[category].push(sel);
// Mostrar sample do primeiro elemento
const sample = elements[0];
const info = {
tagName: sample.tagName,
id: sample.id || '(none)',
className: (sample.className || '').substring(0, 100),
dataTestId: sample.getAttribute('data-testid') || '(none)',
textLength: (sample.textContent || '').length
};
console.log(` Sample:`, info);
}
} catch (e) {
console.log(` ⚠️ ${sel} → ERRO: ${e.message}`);
report.results[category].push({
selector: sel,
error: e.message,
status: 'error'
});
}
}
}
// Análise especial: encontrar todas as estruturas de mensagem
console.log('\n═══════════════════════════════════════════════');
console.log('🔬 ANÁLISE PROFUNDA DA ESTRUTURA');
console.log('═══════════════════════════════════════════════');
this.deepAnalysis(report);
// Gerar recomendações
this.generateRecommendations(report);
// Mostrar resumo
console.log('\n═══════════════════════════════════════════════');
console.log('📊 RESUMO');
console.log('═══════════════════════════════════════════════');
for (const [cat, sels] of Object.entries(report.workingSelectors)) {
if (sels.length > 0) {
console.log(`✅ ${cat}: ${sels[0]}`);
} else {
console.log(`❌ ${cat}: NENHUM SELETOR FUNCIONA`);
}
}
console.log('\n📝 RECOMENDAÇÕES:');
report.recommendations.forEach((r, i) => {
console.log(`${i+1}. ${r}`);
});
// Salvar report
console.log('\n💾 Report salvo em: window.CE_DIAGNOSTIC_REPORT');
window.CE_DIAGNOSTIC_REPORT = report;
// Criar botão para copiar
this.createCopyButton(report);
return report;
},
deepAnalysis(report) {
// Encontrar main
const main = document.querySelector('main');
if (!main) {
console.log('❌ Elemento <main> não encontrado!');
report.recommendations.push('Página pode não estar totalmente carregada');
return;
}
console.log('✅ <main> encontrado');
// Buscar divs com data-testid
const testIdDivs = main.querySelectorAll('[data-testid]');
console.log(`\n📍 Elementos com data-testid: ${testIdDivs.length}`);
const testIds = new Set();
testIdDivs.forEach(el => {
const tid = el.getAttribute('data-testid');
testIds.add(tid);
});
console.log('Unique data-testids:', Array.from(testIds).sort().join(', '));
report.foundTestIds = Array.from(testIds);
// Buscar divs com role
const roleDivs = main.querySelectorAll('[role]');
console.log(`\n📍 Elementos com role: ${roleDivs.length}`);
const roles = new Set();
roleDivs.forEach(el => {
roles.add(el.getAttribute('role'));
});
console.log('Unique roles:', Array.from(roles).sort().join(', '));
report.foundRoles = Array.from(roles);
// Buscar classes comuns
console.log('\n📍 Classes frequentes:');
const classCount = {};
main.querySelectorAll('*').forEach(el => {
(el.className || '').split(/\s+/).forEach(c => {
if (c && c.length > 3) {
// Normalizar classes hashadas
const normalized = c.replace(/[-_][a-z0-9]{5,}$/i, '');
classCount[normalized] = (classCount[normalized] || 0) + 1;
}
});
});
const topClasses = Object.entries(classCount)
.filter(([c, n]) => n > 5)
.sort((a, b) => b[1] - a[1])
.slice(0, 30);
topClasses.forEach(([c, n]) => {
console.log(` ${c}: ${n}x`);
});
report.topClasses = topClasses;
// Detectar estrutura de mensagens por heurística
console.log('\n📍 Tentando detectar mensagens por heurística...');
// Procurar por elementos com "prose" (típico de mensagens)
const proseElements = main.querySelectorAll('[class*="prose"]');
console.log(`Elementos com prose: ${proseElements.length}`);
if (proseElements.length > 0) {
// Subir hierarquia para encontrar container
const sample = proseElements[0];
let parent = sample.parentElement;
let levels = 0;
const hierarchy = [sample.tagName + '.' + (sample.className || '').split(' ')[0]];
while (parent && levels < 10) {
hierarchy.push(parent.tagName + (parent.className ? '.' + parent.className.split(' ')[0] : ''));
parent = parent.parentElement;
levels++;
}
console.log('Hierarquia de prose:', hierarchy.join(' → '));
report.proseHierarchy = hierarchy;
}
},
generateRecommendations(report) {
// Container
if (report.workingSelectors.container.length === 0) {
report.recommendations.push('CRÍTICO: Nenhum container encontrado. Verifique se a conversa está aberta.');
}
// Messages
if (report.workingSelectors.messageGroup.length === 0) {
report.recommendations.push('CRÍTICO: Nenhum grupo de mensagem detectado. Interface pode ter mudado.');
if (report.foundTestIds && report.foundTestIds.length > 0) {
const turnIds = report.foundTestIds.filter(id =>
id.includes('turn') || id.includes('message') || id.includes('conversation')
);
if (turnIds.length > 0) {
report.recommendations.push(`Sugestão: Tentar seletores com: ${turnIds.join(', ')}`);
}
}
}
// Thinking
if (report.workingSelectors.thinking.length === 0) {
report.recommendations.push('INFO: Nenhum thinking detectado. Pode não existir nesta conversa.');
}
// Content
if (report.workingSelectors.content.length === 0) {
report.recommendations.push('ALERTA: Seletor de conteúdo não encontrado. Extração pode falhar.');
}
},
createCopyButton(report) {
const btn = document.createElement('button');
btn.textContent = '📋 Copiar Diagnóstico';
btn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99999;
padding: 12px 20px;
background: #7c3aed;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-family: system-ui;
font-size: 14px;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
btn.onclick = () => {
const text = JSON.stringify(report, null, 2);
navigator.clipboard.writeText(text).then(() => {
btn.textContent = '✅ Copiado!';
setTimeout(() => { btn.textContent = '📋 Copiar Diagnóstico'; }, 2000);
});
};
document.body.appendChild(btn);
}
};
// ═══════════════════════════════════════════════════════════
// AUTO-RUN
// ═══════════════════════════════════════════════════════════
// Esperar página carregar
const waitAndRun = () => {
if (document.querySelector('main')) {
setTimeout(() => {
Diagnostic.run();
}, 2000); // Esperar 2s para garantir carregamento
} else {
setTimeout(waitAndRun, 1000);
}
};
// Rodar
if (document.readyState === 'complete') {
waitAndRun();
} else {
window.addEventListener('load', waitAndRun);
}
// Também expor para execução manual
window.runDiagnostic = () => Diagnostic.run();
console.log('💡 Claude Extractor Diagnostic carregado. Execute window.runDiagnostic() para rodar manualmente.');
})();
// ==UserScript==
// @name Claude Extractor
// @namespace https://github.com/cnmfs/claude-extractor
// @version 2.1.1
// @description Extrai conversas, projetos, mensagens e artefatos do Claude.ai com hierarquia completa
// @author cnmfs
// @match https://claude.ai/*
// @icon https://claude.ai/favicon.ico
// @grant none
// @run-at document-idle
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const VERSION = '2.1.1';
const DEBUG = true; // ATIVADO para diagnóstico
// ═══════════════════════════════════════════════════════════
// CONFIGURAÇÃO
// ═══════════════════════════════════════════════════════════
const DEFAULT_CONFIG = {
version: VERSION,
debug: DEBUG,
ui: {
position: { right: 20, bottom: 80 },
showLabels: true,
collapsed: false,
language: 'pt-BR'
},
export: {
includeMetadata: true,
includeThinking: true,
includeToolCalls: true,
includeCitations: true,
includeConsole: true,
flatMarkdown: false
}
};
let CONFIG = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
// ═══════════════════════════════════════════════════════════
// TRADUÇÕES
// ═══════════════════════════════════════════════════════════
const I18N = {
'pt-BR': {
project: 'Projeto',
conversation: 'Conversa',
message: 'Mensagem',
thinking: 'Thinking',
artifacts: 'Artefatos',
flatMd: 'Flat MD',
settings: 'Config',
diagnose: 'Diagnóstico',
export: 'Exportar',
cancel: 'Cancelar',
close: 'Fechar',
processing: 'Processando...',
extracting: 'Extraindo...',
generating: 'Gerando ZIP...',
noMessages: 'Nenhuma mensagem encontrada',
noThinking: 'Nenhum thinking block',
noArtifacts: 'Nenhum artefato',
noProject: 'Não está em um projeto',
success: 'Sucesso',
error: 'Erro',
previewTitle: 'Preview da Extração',
projectPreview: 'Preview do Projeto',
selectAll: 'Todos',
deselectAll: 'Nenhum',
messages: 'mensagens',
components: 'componentes',
conversations: 'conversas',
files: 'arquivos',
systemPrompt: 'System Prompt',
knowledge: 'Base de Conhecimento',
branches: 'Branches',
selectBranch: 'Selecionar Branch',
mainBranch: 'Principal (M)',
console: 'Console'
},
'en': {
project: 'Project',
conversation: 'Conversation',
message: 'Message',
thinking: 'Thinking',
artifacts: 'Artifacts',
flatMd: 'Flat MD',
settings: 'Settings',
diagnose: 'Diagnose',
export: 'Export',
cancel: 'Cancel',
close: 'Close',
processing: 'Processing...',
extracting: 'Extracting...',
generating: 'Generating ZIP...',
noMessages: 'No messages found',
noThinking: 'No thinking blocks',
noArtifacts: 'No artifacts',
noProject: 'Not in a project',
success: 'Success',
error: 'Error',
previewTitle: 'Extraction Preview',
projectPreview: 'Project Preview',
selectAll: 'All',
deselectAll: 'None',
messages: 'messages',
components: 'components',
conversations: 'conversations',
files: 'files',
systemPrompt: 'System Prompt',
knowledge: 'Knowledge Base',
branches: 'Branches',
selectBranch: 'Select Branch',
mainBranch: 'Main (M)',
console: 'Console'
}
};
const t = (key) => I18N[CONFIG.ui.language]?.[key] || I18N['en'][key] || key;
// ═══════════════════════════════════════════════════════════
// STORAGE
// ═══════════════════════════════════════════════════════════
const Storage = {
prefix: 'ce_',
save: (k, v) => { try { localStorage.setItem(Storage.prefix + k, JSON.stringify(v)); } catch(e) {} },
load: (k, d) => { try { const v = localStorage.getItem(Storage.prefix + k); return v ? JSON.parse(v) : d; } catch(e) { return d; } },
loadConfig: () => { const s = Storage.load('config'); if (s) CONFIG = {...DEFAULT_CONFIG, ...s, ui: {...DEFAULT_CONFIG.ui, ...(s.ui||{})}, export: {...DEFAULT_CONFIG.export, ...(s.export||{})}}; },
saveConfig: () => Storage.save('config', { ui: CONFIG.ui, export: CONFIG.export, debug: CONFIG.debug })
};
// ═══════════════════════════════════════════════════════════
// UTILS
// ═══════════════════════════════════════════════════════════
const Utils = {
log: (...a) => CONFIG.debug && console.log('[CE]', ...a),
warn: (...a) => console.warn('[CE]', ...a),
error: (...a) => console.error('[CE]', ...a),
sanitize: (n) => (n || 'untitled').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[<>:"/\\|?*]/g,'-').replace(/\s+/g,'-').replace(/-+/g,'-').substring(0,60).trim() || 'untitled',
formatDate: () => {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}-${String(d.getHours()).padStart(2,'0')}${String(d.getMinutes()).padStart(2,'0')}`;
},
pad: (n, s=3) => String(n).padStart(s, '0'),
sleep: (ms) => new Promise(r => setTimeout(r, ms)),
// MELHORADO: Query com fallbacks mais robustos
query: (sels, p=document) => {
const selArray = Array.isArray(sels) ? sels : [sels];
for (const s of selArray) {
try {
const e = p.querySelector(s);
if (e) {
Utils.log(`query: "${s}" encontrado`);
return e;
}
} catch(e) {
Utils.log(`query: "${s}" erro: ${e.message}`);
}
}
return null;
},
// MELHORADO: QueryAll com deduplicação
queryAll: (sels, p=document) => {
const r = new Set();
const selArray = Array.isArray(sels) ? sels : [sels];
for (const s of selArray) {
try {
p.querySelectorAll(s).forEach(e => r.add(e));
} catch(e) {
Utils.log(`queryAll: "${s}" erro: ${e.message}`);
}
}
return Array.from(r);
},
extractText: (el) => {
if (!el) return '';
const c = el.cloneNode(true);
c.querySelectorAll('script,style,svg,button,input,form').forEach(e => e.remove());
return (c.textContent || '').trim();
},
// MELHORADO: htmlToMd mais completo
htmlToMd: (el) => {
if (!el) return '';
const proc = (n) => {
if (n.nodeType === 3) return n.textContent;
if (n.nodeType !== 1) return '';
const tag = n.tagName.toLowerCase();
const ch = Array.from(n.childNodes).map(proc).join('');
switch(tag) {
// Headers
case 'h1': return `\n# ${ch}\n`;
case 'h2': return `\n## ${ch}\n`;
case 'h3': return `\n### ${ch}\n`;
case 'h4': return `\n#### ${ch}\n`;
case 'h5': return `\n##### ${ch}\n`;
case 'h6': return `\n###### ${ch}\n`;
// Emphasis
case 'strong': case 'b': return `**${ch}**`;
case 'em': case 'i': return `*${ch}*`;
case 'del': case 's': return `~~${ch}~~`;
case 'u': return `<u>${ch}</u>`;
case 'mark': return `==${ch}==`;
// Code
case 'code':
const isInPre = n.parentElement?.tagName?.toLowerCase() === 'pre';
return isInPre ? ch : `\`${ch}\``;
case 'pre':
const code = n.querySelector('code');
const lang = code?.className?.match(/language-(\w+)/)?.[1] || '';
const content = code ? code.textContent : n.textContent;
return `\n\`\`\`${lang}\n${content}\n\`\`\`\n`;
// Links
case 'a':
const href = n.getAttribute('href') || '';
return href ? `[${ch}](${href})` : ch;
// Lists
case 'ul': return '\n' + ch;
case 'ol': return '\n' + ch;
case 'li':
const parent = n.parentElement?.tagName?.toLowerCase();
const idx = Array.from(n.parentElement?.children || []).indexOf(n);
const prefix = parent === 'ol' ? `${idx + 1}. ` : '- ';
return prefix + ch.trim() + '\n';
// Blocks
case 'p': return `\n${ch}\n`;
case 'div': return ch.includes('\n') ? ch : `${ch}\n`;
case 'br': return '\n';
case 'blockquote': return '\n> ' + ch.trim().replace(/\n/g, '\n> ') + '\n';
case 'hr': return '\n---\n';
// Tables
case 'table': return '\n' + ch + '\n';
case 'thead': return ch;
case 'tbody': return ch;
case 'tr':
const cells = Array.from(n.children).map(c => proc(c).trim().replace(/\|/g, '\\|'));
const row = '| ' + cells.join(' | ') + ' |';
// Adicionar linha separadora após header
if (n.parentElement?.tagName?.toLowerCase() === 'thead') {
const sep = '| ' + cells.map(() => '---').join(' | ') + ' |';
return row + '\n' + sep + '\n';
}
return row + '\n';
case 'th': case 'td': return ch;
// Details/Summary (thinking blocks)
case 'details':
const summary = n.querySelector('summary');
const summaryText = summary ? Utils.extractText(summary) : 'Details';
const detailsContent = Array.from(n.childNodes)
.filter(c => c !== summary)
.map(proc).join('');
return `\n<details>\n<summary>${summaryText}</summary>\n${detailsContent}\n</details>\n`;
case 'summary': return '';
// Media
case 'img':
const alt = n.getAttribute('alt') || 'image';
const src = n.getAttribute('src') || '';
return src ? `![${alt}](${src})` : '';
// Superscript/Subscript
case 'sup': return `^${ch}^`;
case 'sub': return `~${ch}~`;
// Ignore
case 'script': case 'style': case 'svg': case 'button':
case 'input': case 'form': case 'nav': case 'footer':
return '';
// Span with potential classes
case 'span':
const cls = n.className || '';
if (cls.includes('citation')) return `[${ch}]`;
return ch;
// Default: pass through children
default: return ch;
}
};
return proc(el)
.replace(/\n{3,}/g, '\n\n')
.replace(/^\s+|\s+$/gm, '')
.trim();
},
detectLang: (content, lang) => {
const map = {
'python': 'py', 'py': 'py', 'javascript': 'js', 'js': 'js', 'typescript': 'ts', 'ts': 'ts',
'jsx': 'jsx', 'tsx': 'tsx', 'html': 'html', 'css': 'css', 'json': 'json', 'yaml': 'yaml',
'yml': 'yaml', 'sql': 'sql', 'bash': 'sh', 'shell': 'sh', 'sh': 'sh', 'markdown': 'md', 'md': 'md',
'mermaid': 'mermaid', 'java': 'java', 'c': 'c', 'cpp': 'cpp', 'csharp': 'cs', 'go': 'go',
'rust': 'rs', 'ruby': 'rb', 'php': 'php', 'swift': 'swift', 'kotlin': 'kt', 'plaintext': 'txt',
'text': 'txt', 'xml': 'xml', 'toml': 'toml', 'ini': 'ini', 'dockerfile': 'dockerfile',
'makefile': 'makefile', 'powershell': 'ps1', 'r': 'r', 'scala': 'scala', 'lua': 'lua',
'perl': 'pl', 'haskell': 'hs', 'elixir': 'ex', 'clojure': 'clj', 'erlang': 'erl'
};
const l = (lang || '').toLowerCase();
if (map[l]) return map[l];
if (content) {
if (content.includes('def ') && content.includes(':')) return 'py';
if (content.match(/^(function|const|let|var|import|export)/m)) return 'js';
if (content.match(/<html|<!DOCTYPE/i)) return 'html';
if (content.startsWith('{') || content.startsWith('[')) { try { JSON.parse(content); return 'json'; } catch(e) {} }
if (content.match(/^(graph|flowchart|sequenceDiagram|classDiagram)/m)) return 'mermaid';
if (content.match(/^#!/)) return 'sh';
}
return 'txt';
}
};
// ═══════════════════════════════════════════════════════════
// SELETORES DOM v2.1 - ROBUSTOS
// ═══════════════════════════════════════════════════════════
const Selectors = {
// Container principal das mensagens
container: [
'[data-testid="conversation-turn-list"]',
'[data-testid="conversation-messages"]',
// v2.1.1: Novos seletores baseados em diagnóstico
'div[class*="min-h-"]:has([class*="font-claude"])',
'[class*="conversation-content"]',
'[class*="chat-messages"]',
'main [class*="overflow-y-auto"]',
'main [class*="flex-col"]',
'[class*="react-scroll-to-bottom"]',
'main > div > div',
'main'
],
// Grupo de mensagem (cada turno da conversa)
messageGroup: [
'[data-testid^="conversation-turn"]',
'[data-testid*="turn"]',
'[class*="group/conversation-turn"]',
'div[class*="group"][class*="relative"]:has([class*="prose"])',
'div[class*="min-h-"]:has([class*="font-claude"])',
'div.group:has([class*="prose"])'
],
// Identificador de mensagem humana
human: [
'[data-testid="user-message"]',
'[data-testid="human-turn"]',
'[data-testid*="human"]',
'[data-testid*="user"]',
'[class*="human-turn"]',
'[class*="user-message"]'
],
// Identificador de mensagem do assistente
assistant: [
'[data-testid="assistant-message"]',
'[data-testid="assistant-turn"]',
'[data-testid*="assistant"]',
'[class*="assistant-turn"]',
'[class*="ai-message"]'
],
// Conteúdo da mensagem
content: [
'[class*="prose"]',
'[class*="markdown"]',
'[class*="font-claude-message"]',
'[class*="message-content"]',
'.whitespace-pre-wrap'
],
// Thinking blocks
thinking: [
'[data-testid="thinking-block"]',
'[data-testid="thinking-summary"]',
'[data-testid="extended-thinking"]',
'[data-testid*="thinking"]',
'details[class*="thinking"]',
'[class*="thinking-container"]',
'details:has(summary)'
],
// Artefatos/código
artifact: [
'[data-testid="artifact"]',
'[data-artifact-id]',
'[class*="artifact-container"]',
'pre:has(code)'
],
// Chamadas de ferramenta
tools: [
'[data-testid="tool-use"]',
'[data-testid="tool-call"]',
'[data-testid*="tool"]',
'[class*="tool-use"]',
'[class*="function-call"]'
],
// Citações
citation: [
'[data-testid="citation"]',
'[class*="citation"]',
'a[href^="http"]:not([href*="claude.ai"])'
],
// Console/Output
console: [
'[data-testid="code-output"]',
'[data-testid="execution-result"]',
'[class*="code-output"]',
'[class*="execution-result"]',
'[class*="console-output"]',
'pre[class*="output"]',
'[class*="stdout"]'
],
// Branches
branch: [
'[data-testid="branch-selector"]',
'[data-testid="message-versions"]',
'[class*="message-versions"]',
'[class*="branch-indicator"]',
'button[aria-label*="version"]'
],
// Projeto
project: {
container: [
'[data-testid="project-container"]',
'[class*="project-page"]',
'main[class*="project"]'
],
title: [
'[data-testid="project-title"]',
'h1[class*="project"]',
'[class*="project-header"] h1'
],
systemPrompt: [
'[data-testid="project-instructions"]',
'[data-testid="system-prompt"]',
'textarea[name="instructions"]',
'[class*="system-prompt"]',
'[class*="project-instructions"]'
],
knowledge: [
'[data-testid="knowledge-file"]',
'[data-testid="project-file"]',
'[class*="knowledge-item"]',
'[class*="file-item"]'
],
conversationItem: [
'[data-testid="conversation-item"]',
'[data-testid="chat-item"]',
'a[href*="/chat/"]'
]
}
};
// ═══════════════════════════════════════════════════════════
// DETECTOR v2.1 - ROBUSTO
// ═══════════════════════════════════════════════════════════
const Detector = {
cache: null,
cacheTime: 0,
detect(force = false) {
const now = Date.now();
if (!force && this.cache && (now - this.cacheTime) < 3000) return this.cache;
Utils.log('=== DETECTANDO ESTRUTURA DOM v2.1.1 ===');
const result = {
groups: [],
hasThinking: false,
hasArtifacts: false,
hasTools: false,
hasConsole: false,
hasBranches: false,
containerFound: false,
rawGroupCount: 0,
searchRootUsed: 'none', // v2.1.1: Track which root was used
usedBodyFallback: false // v2.1.1: Track if body fallback was needed
};
// 1. Encontrar container
const container = Utils.query(Selectors.container);
if (!container) {
Utils.warn('Container não encontrado - tentando fallback');
// Fallback: usar main diretamente
const main = document.querySelector('main');
if (!main) {
Utils.error('Elemento main não encontrado!');
return result;
}
Utils.log('Usando main como container fallback');
} else {
Utils.log('Container encontrado:', container.tagName, container.className?.substring(0, 50));
result.containerFound = true;
}
let searchRoot = container || document.querySelector('main') || document.body;
result.searchRootUsed = container ? 'container' : (document.querySelector('main') ? 'main' : 'body');
// 2. Encontrar grupos de mensagens
let groups = Utils.queryAll(Selectors.messageGroup, searchRoot);
Utils.log(`Encontrados ${groups.length} grupos via seletores primários em ${result.searchRootUsed}`);
// PATCH v2.1.1: Se searchRoot restrito não encontrou, buscar no body inteiro
if (groups.length === 0 && searchRoot !== document.body) {
Utils.log('SearchRoot restrito falhou - buscando em document.body...');
groups = Utils.queryAll(Selectors.messageGroup, document.body);
Utils.log(`Encontrados ${groups.length} grupos em document.body`);
if (groups.length > 0) {
searchRoot = document.body;
result.searchRootUsed = 'body-fallback';
result.usedBodyFallback = true;
}
}
// Fallback final: buscar divs com conteúdo de mensagem
if (groups.length === 0) {
Utils.log('Aplicando fallback de detecção em document.body...');
groups = this.fallbackDetection(document.body);
if (groups.length > 0) {
result.searchRootUsed = 'body-fallback-detection';
result.usedBodyFallback = true;
}
}
result.rawGroupCount = groups.length;
Utils.log(`Total de grupos após fallback: ${groups.length}`);
// 3. Processar cada grupo
groups.forEach((group, idx) => {
const isHuman = this.detectAuthor(group) === 'USER';
const isAssistant = this.detectAuthor(group) === 'AI';
// Skip se não identificado e sem conteúdo significativo
if (!isHuman && !isAssistant) {
const hasProseContent = group.querySelector('[class*="prose"]');
const hasCode = group.querySelector('pre code');
const textLen = (group.textContent || '').length;
if (!hasProseContent && !hasCode && textLen < 100) {
Utils.log(`Grupo ${idx} ignorado: sem conteúdo significativo`);
return;
}
}
const hasThinking = Utils.query(Selectors.thinking, group) !== null;
const hasArtifacts = group.querySelectorAll('pre code').length > 0 || Utils.query(Selectors.artifact, group) !== null;
const hasTools = Utils.query(Selectors.tools, group) !== null;
const hasConsole = Utils.query(Selectors.console, group) !== null;
const hasBranches = this.detectBranches(group).length > 1;
result.groups.push({
element: group,
index: idx,
isHuman,
isAssistant: isAssistant || !isHuman,
hasThinking,
hasArtifacts,
hasTools,
hasConsole,
hasBranches,
branches: hasBranches ? this.detectBranches(group) : ['M']
});
if (hasThinking) result.hasThinking = true;
if (hasArtifacts) result.hasArtifacts = true;
if (hasTools) result.hasTools = true;
if (hasConsole) result.hasConsole = true;
if (hasBranches) result.hasBranches = true;
});
this.cache = result;
this.cacheTime = now;
Utils.log(`=== DETECÇÃO COMPLETA: ${result.groups.length} grupos válidos ===`);
return result;
},
// Fallback de detecção quando seletores primários falham
fallbackDetection(root) {
const groups = [];
// Estratégia 1: Buscar elementos com prose
const proseElements = root.querySelectorAll('[class*="prose"]');
Utils.log(`Fallback: ${proseElements.length} elementos prose`);
proseElements.forEach(prose => {
// Subir até encontrar o container da mensagem
let parent = prose.parentElement;
let levels = 0;
while (parent && levels < 5) {
const cls = parent.className || '';
const hasGroup = cls.includes('group') ||
cls.includes('relative') ||
parent.hasAttribute('data-testid');
if (hasGroup && !groups.includes(parent)) {
groups.push(parent);
break;
}
parent = parent.parentElement;
levels++;
}
});
// Estratégia 2: Buscar divs com data-testid relacionados
if (groups.length === 0) {
root.querySelectorAll('[data-testid]').forEach(el => {
const testId = el.getAttribute('data-testid') || '';
if (testId.includes('turn') || testId.includes('message')) {
if (!groups.includes(el)) groups.push(el);
}
});
}
// Estratégia 3: Buscar pela estrutura de código
if (groups.length === 0) {
root.querySelectorAll('pre code').forEach(code => {
let parent = code.closest('div[class*="group"]') ||
code.closest('div[class*="relative"]');
if (parent && !groups.includes(parent)) {
groups.push(parent);
}
});
}
Utils.log(`Fallback retornou ${groups.length} grupos`);
return groups;
},
// MELHORADO: Detecção robusta de autor
detectAuthor(group) {
// 1. data-testid
const testId = (group.getAttribute('data-testid') || '').toLowerCase();
if (testId.includes('human') || testId.includes('user')) return 'USER';
if (testId.includes('assistant') || testId.includes('ai')) return 'AI';
// 2. Filhos com data-testid
if (group.querySelector('[data-testid*="user"]') ||
group.querySelector('[data-testid*="human"]')) return 'USER';
if (group.querySelector('[data-testid*="assistant"]')) return 'AI';
// 3. Classes
const cls = (group.className || '').toLowerCase();
if (cls.includes('human') || cls.includes('user')) return 'USER';
if (cls.includes('assistant') || cls.includes('ai')) return 'AI';
// 4. aria-label
const ariaLabel = (group.getAttribute('aria-label') || '').toLowerCase();
if (ariaLabel.includes('you') || ariaLabel.includes('user')) return 'USER';
if (ariaLabel.includes('claude') || ariaLabel.includes('assistant')) return 'AI';
// 5. Heurística: mensagens com thinking/artifacts/code são do AI
if (group.querySelector('details') ||
group.querySelector('[class*="thinking"]') ||
group.querySelectorAll('pre code').length > 0) {
return 'AI';
}
// 6. Heurística: texto curto sem formatação provavelmente é humano
const textContent = Utils.extractText(group);
const hasFormatting = group.querySelector('h1,h2,h3,ul,ol,table,blockquote');
if (textContent.length < 500 && !hasFormatting) {
return 'USER';
}
return 'UNKNOWN';
},
detectBranches(group) {
const branches = ['M'];
const branchElements = Utils.queryAll(Selectors.branch, group);
if (branchElements.length === 0) return branches;
branchElements.forEach((el, idx) => {
const text = el.textContent || el.getAttribute('aria-label') || '';
const versionMatch = text.match(/(?:vers[ãa]o|version)\s*(\d+)/i);
if (versionMatch) {
branches.push(`T${Utils.pad(parseInt(versionMatch[1]), 2)}`);
} else {
branches.push(`T${Utils.pad(idx + 1, 2)}`);
}
});
return [...new Set(branches)];
},
detectProjectContext() {
const url = window.location.href;
const projectMatch = url.match(/\/project\/([a-f0-9-]+)/i);
if (!projectMatch) {
return { isProject: false, projectId: null, chatId: null };
}
const chatMatch = url.match(/\/chat\/([a-f0-9-]+)/i);
return {
isProject: true,
projectId: projectMatch[1],
chatId: chatMatch ? chatMatch[1] : null,
isProjectHome: !chatMatch
};
},
// DIAGNÓSTICO COMPLETO
diagnose() {
const report = {
url: window.location.href,
timestamp: new Date().toISOString(),
version: VERSION,
projectContext: this.detectProjectContext(),
selectors: {},
recommendations: []
};
console.log('═══════════════════════════════════════════════');
console.log('🔍 CLAUDE EXTRACTOR v' + VERSION + ' - DIAGNÓSTICO');
console.log('═══════════════════════════════════════════════');
console.log('URL:', report.url);
// Testar cada categoria
for (const [name, sels] of Object.entries(Selectors)) {
if (typeof sels === 'object' && !Array.isArray(sels)) {
// Nested (project)
report.selectors[name] = {};
for (const [subName, subSels] of Object.entries(sels)) {
report.selectors[name][subName] = this.testSelectors(subSels, name + '.' + subName);
}
} else {
report.selectors[name] = this.testSelectors(sels, name);
}
}
// Análise especial
const structure = this.detect(true);
report.detection = {
containerFound: structure.containerFound,
groupsFound: structure.groups.length,
rawGroupCount: structure.rawGroupCount,
searchRootUsed: structure.searchRootUsed,
usedBodyFallback: structure.usedBodyFallback,
hasThinking: structure.hasThinking,
hasArtifacts: structure.hasArtifacts,
hasTools: structure.hasTools,
hasConsole: structure.hasConsole,
hasBranches: structure.hasBranches
};
console.log('\n📊 RESULTADO DA DETECÇÃO:');
console.log(' Container:', structure.containerFound ? '✅' : '❌');
console.log(' SearchRoot:', structure.searchRootUsed);
console.log(' Body Fallback:', structure.usedBodyFallback ? '⚠️ SIM' : 'Não');
console.log(' Grupos Brutos:', structure.rawGroupCount);
console.log(' Grupos Válidos:', structure.groups.length);
console.log(' Thinking:', structure.hasThinking ? '✅' : '❌');
console.log(' Artifacts:', structure.hasArtifacts ? '✅' : '❌');
// Recomendações
if (structure.groups.length === 0) {
report.recommendations.push('CRÍTICO: Nenhum grupo detectado. Verifique se há mensagens na conversa.');
}
if (!structure.containerFound) {
report.recommendations.push('ALERTA: Container principal não encontrado. Usando fallback.');
}
console.log('\n💡 RECOMENDAÇÕES:');
report.recommendations.forEach(r => console.log(' -', r));
window.CE_DIAGNOSTIC = report;
console.log('\n💾 Report completo em: window.CE_DIAGNOSTIC');
return report;
},
testSelectors(sels, name) {
const result = { working: [], failed: [] };
console.log(`\n📋 ${name}:`);
for (const sel of (Array.isArray(sels) ? sels : [sels])) {
try {
const count = document.querySelectorAll(sel).length;
if (count > 0) {
result.working.push({ selector: sel, count });
console.log(` ✅ ${sel}: ${count}`);
} else {
result.failed.push(sel);
console.log(` ❌ ${sel}: 0`);
}
} catch(e) {
result.failed.push(sel);
console.log(` ⚠️ ${sel}: ERRO`);
}
}
return result;
}
};
// ═══════════════════════════════════════════════════════════
// EXTRACTOR v2.1
// ═══════════════════════════════════════════════════════════
const Extractor = {
extractContent(group) {
const contentEl = Utils.query(Selectors.content, group);
if (contentEl) {
const md = Utils.htmlToMd(contentEl);
Utils.log(`extractContent: ${md.length} chars via seletor`);
return md;
}
// Fallback: extrair texto limpo
const text = Utils.extractText(group);
Utils.log(`extractContent: ${text.length} chars via fallback`);
return text.length > 10 ? text : '';
},
extractThinking(group) {
const blocks = [];
const containers = Utils.queryAll(Selectors.thinking, group);
Utils.log(`extractThinking: ${containers.length} containers encontrados`);
containers.forEach((c, idx) => {
// Expandir details
if (c.tagName === 'DETAILS' && !c.open) {
c.open = true;
}
let content = '';
const clone = c.cloneNode(true);
clone.querySelector('summary')?.remove();
content = Utils.extractText(clone);
if (content && content.length > 20) {
blocks.push({ index: idx + 1, content });
Utils.log(` Thinking block ${idx+1}: ${content.length} chars`);
}
});
return blocks;
},
extractArtifacts(group) {
const artifacts = [];
const seen = new Set();
// Containers de artefato
Utils.queryAll(Selectors.artifact, group).forEach(c => {
c.querySelectorAll('pre code, code').forEach(code => {
const content = code.textContent || '';
if (content.length < 10 || seen.has(content)) return;
seen.add(content);
const langMatch = code.className.match(/language-(\w+)/);
const lang = langMatch ? langMatch[1] : '';
const ext = Utils.detectLang(content, lang);
artifacts.push({ index: artifacts.length + 1, content, language: lang, ext });
});
});
// Pre code diretos
group.querySelectorAll('pre').forEach(pre => {
const code = pre.querySelector('code') || pre;
const content = code.textContent || '';
if (content.length < 10 || seen.has(content)) return;
seen.add(content);
const langMatch = (code.className + ' ' + pre.className).match(/language-(\w+)/);
const lang = langMatch ? langMatch[1] : '';
const ext = Utils.detectLang(content, lang);
artifacts.push({ index: artifacts.length + 1, content, language: lang, ext });
});
Utils.log(`extractArtifacts: ${artifacts.length} artefatos`);
return artifacts;
},
extractTools(group) {
const tools = [];
Utils.queryAll(Selectors.tools, group).forEach((el, idx) => {
const content = Utils.extractText(el);
if (content) tools.push({ type: 'tool', index: idx + 1, content });
});
Utils.log(`extractTools: ${tools.length} tools`);
return tools;
},
extractCitations(group) {
const citations = [];
const seen = new Set();
Utils.queryAll(Selectors.citation, group).forEach(el => {
const href = el.getAttribute('href');
if (!href || !href.startsWith('http') || seen.has(href)) return;
seen.add(href);
citations.push({ text: el.textContent?.trim() || href, url: href });
});
return citations;
},
extractConsole(group) {
const outputs = [];
Utils.queryAll(Selectors.console, group).forEach((el, idx) => {
const content = el.textContent?.trim() || '';
if (content.length < 5) return;
let outputType = 'stdout';
const cls = (el.className || '').toLowerCase();
if (cls.includes('stderr') || cls.includes('error')) {
outputType = 'stderr';
}
outputs.push({
index: idx + 1,
type: outputType,
content
});
});
Utils.log(`extractConsole: ${outputs.length} outputs`);
return outputs;
},
extractAll(selectedBranch = 'M') {
Utils.log('=== EXTRAINDO MENSAGENS ===');
const messages = [];
const structure = Detector.detect(true);
if (structure.groups.length === 0) {
Utils.warn('Nenhum grupo detectado');
return messages;
}
structure.groups.forEach((g, idx) => {
const author = g.isHuman ? 'USER' : 'AI';
let branch = selectedBranch;
if (g.hasBranches && g.branches.includes(selectedBranch)) {
branch = selectedBranch;
} else {
branch = 'M';
}
const msg = {
index: idx + 1,
author,
branch,
availableBranches: g.branches || ['M'],
components: [],
stats: { thinking: 0, artifacts: 0, tools: 0, citations: 0, console: 0 }
};
let seq = 0;
// Thinking
if (author === 'AI' && CONFIG.export.includeThinking) {
const thinking = this.extractThinking(g.element);
thinking.forEach(tb => {
seq++;
msg.components.push({ seq, type: 'thinking', content: tb.content, ext: 'md' });
msg.stats.thinking++;
});
}
// Conteúdo principal
const content = this.extractContent(g.element);
if (content && content.length > 0) {
seq++;
msg.components.push({ seq, type: 'message', content, ext: 'md' });
}
// Artefatos
const artifacts = this.extractArtifacts(g.element);
artifacts.forEach(a => {
seq++;
msg.components.push({ seq, type: 'artifact', content: a.content, language: a.language, ext: a.ext });
msg.stats.artifacts++;
});
// Tools
if (author === 'AI' && CONFIG.export.includeToolCalls) {
const tools = this.extractTools(g.element);
tools.forEach(t => {
seq++;
msg.components.push({ seq, type: 'tool', content: t.content, ext: 'json' });
msg.stats.tools++;
});
}
// Citations
if (CONFIG.export.includeCitations) {
const citations = this.extractCitations(g.element);
if (citations.length > 0) {
seq++;
const citContent = citations.map(c => `- [${c.text}](${c.url})`).join('\n');
msg.components.push({ seq, type: 'citations', content: citContent, ext: 'md' });
msg.stats.citations = citations.length;
}
}
// Console
if (author === 'AI' && CONFIG.export.includeConsole) {
const consoleOutputs = this.extractConsole(g.element);
consoleOutputs.forEach(co => {
seq++;
msg.components.push({ seq, type: 'console', content: co.content, outputType: co.type, ext: 'txt' });
msg.stats.console++;
});
}
if (msg.components.length > 0) {
messages.push(msg);
}
});
Utils.log(`=== EXTRAÇÃO COMPLETA: ${messages.length} mensagens ===`);
return messages;
},
extractProjectInfo() {
Utils.log('Extraindo info do projeto...');
const info = {
title: '',
systemPrompt: '',
knowledge: [],
conversations: []
};
// Título
const titleEl = Utils.query(Selectors.project.title);
if (titleEl) {
info.title = Utils.extractText(titleEl);
Utils.log('Título:', info.title);
}
// System Prompt
const promptEl = Utils.query(Selectors.project.systemPrompt);
if (promptEl) {
info.systemPrompt = promptEl.value || Utils.extractText(promptEl);
Utils.log('System prompt:', info.systemPrompt.length, 'chars');
}
// Knowledge files
const fileElements = Utils.queryAll(Selectors.project.knowledge);
Utils.log('Knowledge elements:', fileElements.length);
fileElements.forEach((el, idx) => {
const name = Utils.extractText(el) || `file_${idx+1}`;
info.knowledge.push({ index: idx + 1, name, element: el });
});
// Conversations
const convElements = Utils.queryAll(Selectors.project.conversationItem);
Utils.log('Conversation elements:', convElements.length);
convElements.forEach((el, idx) => {
const title = Utils.extractText(el) || `chat_${idx+1}`;
const href = el.getAttribute('href') || '';
const chatIdMatch = href.match(/\/chat\/([a-f0-9-]+)/i);
info.conversations.push({
index: idx + 1,
title,
chatId: chatIdMatch ? chatIdMatch[1] : null,
href
});
});
return info;
},
summary(msgs) {
return {
total: msgs.length,
user: msgs.filter(m => m.author === 'USER').length,
ai: msgs.filter(m => m.author === 'AI').length,
thinking: msgs.reduce((s, m) => s + m.stats.thinking, 0),
artifacts: msgs.reduce((s, m) => s + m.stats.artifacts, 0),
tools: msgs.reduce((s, m) => s + m.stats.tools, 0),
console: msgs.reduce((s, m) => s + (m.stats.console || 0), 0)
};
}
};
// ═══════════════════════════════════════════════════════════
// FORMATTER
// ═══════════════════════════════════════════════════════════
const Formatter = {
message(msg, opts = {}) {
const { includeHeader = true, flat = false } = opts;
let content = '';
if (includeHeader) {
const icon = msg.author === 'USER' ? '👤' : '🤖';
content += `## ${icon} ${msg.author} [${Utils.pad(msg.index)}]\n\n`;
}
msg.components.forEach(comp => {
switch(comp.type) {
case 'thinking':
content += `<details>\n<summary>💭 Thinking</summary>\n\n${comp.content}\n\n</details>\n\n`;
break;
case 'message':
content += comp.content + '\n\n';
break;
case 'artifact':
const lang = comp.language || comp.ext || '';
content += `\`\`\`${lang}\n${comp.content}\n\`\`\`\n\n`;
break;
case 'tool':
content += `<details>\n<summary>🔧 Tool Call</summary>\n\n\`\`\`json\n${comp.content}\n\`\`\`\n\n</details>\n\n`;
break;
case 'citations':
content += `**📚 Citations:**\n${comp.content}\n\n`;
break;
case 'console':
const consoleType = comp.outputType === 'stderr' ? '❌ stderr' : '📤 stdout';
content += `**${consoleType}:**\n\`\`\`\n${comp.content}\n\`\`\`\n\n`;
break;
}
});
return content;
},
conversation(msgs, title = 'Conversation') {
let content = `# ${title}\n\n`;
content += `> Extracted: ${new Date().toISOString()}\n`;
content += `> Messages: ${msgs.length}\n\n`;
content += '---\n\n';
msgs.forEach(msg => {
content += this.message(msg);
content += '---\n\n';
});
return content;
},
flat(msgs, title = 'Conversation') {
return this.conversation(msgs, title);
}
};
// ═══════════════════════════════════════════════════════════
// EXPORTER
// ═══════════════════════════════════════════════════════════
const Exporter = {
async toZip(msgs, options = {}) {
const { name = 'claude-export', includeFlat = true } = options;
if (typeof JSZip === 'undefined') {
throw new Error('JSZip não carregado. Recarregue a página.');
}
const zip = new JSZip();
const baseName = Utils.sanitize(name);
const folder = zip.folder(baseName);
// Metadata
const meta = {
version: VERSION,
exported: new Date().toISOString(),
url: window.location.href,
summary: Extractor.summary(msgs)
};
folder.file('_metadata.json', JSON.stringify(meta, null, 2));
// Mensagens individuais
const messagesFolder = folder.folder('messages');
msgs.forEach(msg => {
const msgFolder = messagesFolder.folder(`${Utils.pad(msg.index)}_${msg.author}`);
msg.components.forEach(comp => {
const filename = `${Utils.pad(comp.seq)}_${comp.type}.${comp.ext}`;
msgFolder.file(filename, comp.content);
});
});
// Flat markdown
if (includeFlat) {
const flatContent = Formatter.flat(msgs, baseName);
folder.file('_full_conversation.md', flatContent);
}
// Gerar ZIP
const blob = await zip.generateAsync({ type: 'blob' });
return { blob, filename: `${baseName}-${Utils.formatDate()}.zip` };
},
async download(msgs, options = {}) {
try {
const { blob, filename } = await this.toZip(msgs, options);
if (typeof saveAs === 'function') {
saveAs(blob, filename);
} else {
// Fallback
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return { success: true, filename };
} catch (e) {
Utils.error('Erro no download:', e);
return { success: false, error: e.message };
}
},
downloadFlat(msgs, options = {}) {
const { name = 'claude-conversation' } = options;
const content = Formatter.flat(msgs, name);
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
const filename = `${Utils.sanitize(name)}-${Utils.formatDate()}.md`;
if (typeof saveAs === 'function') {
saveAs(blob, filename);
} else {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return { success: true, filename };
}
};
// ═══════════════════════════════════════════════════════════
// UI
// ═══════════════════════════════════════════════════════════
const UI = {
container: null,
modal: null,
init() {
if (this.container) return;
this.injectStyles();
this.createContainer();
this.createButtons();
Utils.log('UI inicializada');
},
injectStyles() {
const style = document.createElement('style');
style.textContent = `
.ce-container {
position: fixed;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 8px;
font-family: system-ui, -apple-system, sans-serif;
}
.ce-btn {
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.ce-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.ce-btn-primary {
background: linear-gradient(135deg, #7c3aed, #5b21b6);
color: white;
}
.ce-btn-secondary {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
}
.ce-btn-success {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.ce-btn-warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
}
.ce-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100000;
}
.ce-modal-content {
background: white;
border-radius: 16px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: auto;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}
.ce-modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.ce-modal-header h2 {
margin: 0;
font-size: 18px;
color: #111827;
}
.ce-modal-body {
padding: 20px;
}
.ce-modal-footer {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.ce-stat {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f3f4f6;
}
.ce-stat:last-child {
border-bottom: none;
}
.ce-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6b7280;
padding: 0;
line-height: 1;
}
.ce-close:hover {
color: #111827;
}
.ce-code {
background: #f3f4f6;
padding: 12px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
overflow: auto;
max-height: 300px;
}
`;
document.head.appendChild(style);
},
createContainer() {
this.container = document.createElement('div');
this.container.className = 'ce-container';
this.container.style.right = CONFIG.ui.position.right + 'px';
this.container.style.bottom = CONFIG.ui.position.bottom + 'px';
document.body.appendChild(this.container);
},
createButtons() {
const buttons = [
{ icon: '📥', label: t('export'), action: () => this.showExportPreview(), class: 'ce-btn-primary' },
{ icon: '📄', label: t('flatMd'), action: () => this.exportFlat(), class: 'ce-btn-secondary' },
{ icon: '💭', label: t('thinking'), action: () => this.showThinkingPreview(), class: 'ce-btn-secondary' },
{ icon: '🔍', label: t('diagnose'), action: () => this.showDiagnose(), class: 'ce-btn-warning' }
];
buttons.forEach(btn => {
const el = document.createElement('button');
el.className = `ce-btn ${btn.class}`;
el.innerHTML = `${btn.icon}${CONFIG.ui.showLabels ? ' ' + btn.label : ''}`;
el.onclick = btn.action;
this.container.appendChild(el);
});
},
showModal(title, content, actions = []) {
this.closeModal();
this.modal = document.createElement('div');
this.modal.className = 'ce-modal';
this.modal.innerHTML = `
<div class="ce-modal-content">
<div class="ce-modal-header">
<h2>${title}</h2>
<button class="ce-close">&times;</button>
</div>
<div class="ce-modal-body">${content}</div>
<div class="ce-modal-footer"></div>
</div>
`;
this.modal.querySelector('.ce-close').onclick = () => this.closeModal();
this.modal.onclick = (e) => { if (e.target === this.modal) this.closeModal(); };
const footer = this.modal.querySelector('.ce-modal-footer');
actions.forEach(act => {
const btn = document.createElement('button');
btn.className = `ce-btn ${act.class || 'ce-btn-secondary'}`;
btn.textContent = act.label;
btn.onclick = act.action;
footer.appendChild(btn);
});
document.body.appendChild(this.modal);
},
closeModal() {
if (this.modal) {
this.modal.remove();
this.modal = null;
}
},
showExportPreview() {
const msgs = Extractor.extractAll();
const summary = Extractor.summary(msgs);
if (msgs.length === 0) {
this.showModal(t('error'), `<p>❌ ${t('noMessages')}</p>`, [
{ label: t('close'), action: () => this.closeModal() }
]);
return;
}
const content = `
<div class="ce-stat"><span>📝 ${t('messages')}</span><strong>${summary.total}</strong></div>
<div class="ce-stat"><span>👤 User</span><strong>${summary.user}</strong></div>
<div class="ce-stat"><span>🤖 AI</span><strong>${summary.ai}</strong></div>
<div class="ce-stat"><span>💭 Thinking</span><strong>${summary.thinking}</strong></div>
<div class="ce-stat"><span>📦 Artifacts</span><strong>${summary.artifacts}</strong></div>
<div class="ce-stat"><span>🔧 Tools</span><strong>${summary.tools}</strong></div>
<div class="ce-stat"><span>📤 Console</span><strong>${summary.console}</strong></div>
`;
this.showModal(t('previewTitle'), content, [
{ label: t('cancel'), action: () => this.closeModal() },
{ label: `📥 ${t('export')} ZIP`, class: 'ce-btn-success', action: async () => {
const result = await Exporter.download(msgs, { name: this.getConversationTitle() });
if (result.success) {
this.closeModal();
this.toast(`✅ ${result.filename}`);
} else {
this.toast(`❌ ${result.error}`, 'error');
}
}}
]);
},
showThinkingPreview() {
const msgs = Extractor.extractAll();
const thinkingBlocks = msgs.flatMap(m =>
m.components.filter(c => c.type === 'thinking')
);
if (thinkingBlocks.length === 0) {
this.showModal(t('thinking'), `<p>❌ ${t('noThinking')}</p>`, [
{ label: t('close'), action: () => this.closeModal() }
]);
return;
}
const content = thinkingBlocks.map((tb, idx) =>
`<details style="margin-bottom:12px">
<summary style="cursor:pointer;font-weight:500">💭 Block ${idx+1} (${tb.content.length} chars)</summary>
<div class="ce-code" style="margin-top:8px">${tb.content.substring(0, 500)}${tb.content.length > 500 ? '...' : ''}</div>
</details>`
).join('');
this.showModal(`💭 ${t('thinking')} (${thinkingBlocks.length})`, content, [
{ label: t('close'), action: () => this.closeModal() }
]);
},
showDiagnose() {
const report = Detector.diagnose();
let content = `<p><strong>URL:</strong> ${report.url}</p>`;
content += `<p><strong>Version:</strong> ${report.version}</p>`;
content += `<h3 style="margin-top:16px">📊 Detecção</h3>`;
content += `<div class="ce-stat"><span>Container</span><strong>${report.detection.containerFound ? '✅' : '❌'}</strong></div>`;
content += `<div class="ce-stat"><span>Grupos encontrados</span><strong>${report.detection.groupsFound}</strong></div>`;
content += `<div class="ce-stat"><span>Thinking</span><strong>${report.detection.hasThinking ? '✅' : '❌'}</strong></div>`;
content += `<div class="ce-stat"><span>Artifacts</span><strong>${report.detection.hasArtifacts ? '✅' : '❌'}</strong></div>`;
if (report.recommendations.length > 0) {
content += `<h3 style="margin-top:16px">💡 Recomendações</h3>`;
content += `<ul>${report.recommendations.map(r => `<li>${r}</li>`).join('')}</ul>`;
}
content += `<h3 style="margin-top:16px">📋 Report Completo</h3>`;
content += `<p>Abra o console (F12) ou copie: <code>window.CE_DIAGNOSTIC</code></p>`;
this.showModal('🔍 ' + t('diagnose'), content, [
{ label: '📋 Copiar JSON', action: () => {
navigator.clipboard.writeText(JSON.stringify(report, null, 2));
this.toast('✅ Copiado!');
}},
{ label: t('close'), action: () => this.closeModal() }
]);
},
exportFlat() {
const msgs = Extractor.extractAll();
if (msgs.length === 0) {
this.toast(`❌ ${t('noMessages')}`, 'error');
return;
}
const result = Exporter.downloadFlat(msgs, { name: this.getConversationTitle() });
if (result.success) {
this.toast(`✅ ${result.filename}`);
}
},
getConversationTitle() {
// Tentar obter título da página
const h1 = document.querySelector('h1');
if (h1) return Utils.extractText(h1);
// Fallback para título da página
const title = document.title.replace(/Claude/gi, '').trim();
if (title) return title;
return 'claude-conversation';
},
toast(message, type = 'success') {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
background: ${type === 'error' ? '#ef4444' : '#10b981'};
color: white;
border-radius: 8px;
font-family: system-ui;
font-size: 14px;
z-index: 100001;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
};
// ═══════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════
function init() {
Storage.loadConfig();
// Aguardar página carregar
const waitForPage = () => {
if (document.querySelector('main')) {
Utils.log('=== CLAUDE EXTRACTOR v' + VERSION + ' INICIADO ===');
Utils.log('URL:', location.href);
// Inicializar UI após pequeno delay
setTimeout(() => {
UI.init();
// Rodar diagnóstico inicial
const structure = Detector.detect(true);
Utils.log('Diagnóstico inicial:', {
container: structure.containerFound,
groups: structure.groups.length,
thinking: structure.hasThinking,
artifacts: structure.hasArtifacts
});
}, 1000);
} else {
setTimeout(waitForPage, 500);
}
};
if (document.readyState === 'complete') {
waitForPage();
} else {
window.addEventListener('load', waitForPage);
}
}
// Expor para debug
window.ClaudeExtractor = {
VERSION,
Detector,
Extractor,
Formatter,
Exporter,
UI,
Utils,
diagnose: () => Detector.diagnose(),
extract: () => Extractor.extractAll()
};
init();
})();
// ==UserScript==
// @name Z.ai Mobile Collector Pro
// @namespace ZaiIntelligence
// @version 6.0.0
// @description Coletor de dados otimizado para chat.z.ai (Mobile). Captura diálogos e execuções de forma resiliente.
// @author Z.ai Engineering
// @match *://chat.z.ai/*
// @icon https://chat.z.ai/favicon.ico
// @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js
// @grant GM_addStyle
// @grant GM_setClipboard
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const STATE = {
data: [],
isExpanded: false,
lastCaptureCount: 0
};
// ==========================================
// Módulo 1: UI Mobile-First (Resiliente)
// ==========================================
function injectMobileUI() {
const container = document.createElement('div');
container.id = 'zai-mobile-wrapper';
container.innerHTML = `
<div id="zai-minimized">Z</div>
<div id="zai-expanded" style="display:none;">
<div class="zai-header">
<span>Z.ai Collector (Mobile)</span>
<button id="zai-close-btn">_</button>
</div>
<div class="zai-content">
<div id="zai-count">Itens: 0</div>
<div id="zai-status">Monitorando chat.z.ai...</div>
<div class="zai-actions">
<button id="zai-copy">Copiar JSON</button>
<button id="zai-save" class="primary">Salvar .JSON</button>
</div>
</div>
</div>
`;
document.body.appendChild(container);
GM_addStyle(`
#zai-mobile-wrapper { position: fixed; top: 10px; right: 10px; z-index: 99999; font-family: sans-serif; }
#zai-minimized { width: 44px; height: 44px; background: #00ff88; color: #000; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: pointer; border: 2px solid #000; }
#zai-expanded { width: 280px; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; color: #fff; overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
.zai-header { padding: 10px; background: #000; display: flex; justify-content: space-between; align-items: center; font-size: 12px; border-bottom: 1px solid #333; }
#zai-close-btn { background: none; border: none; color: #fff; font-size: 20px; cursor: pointer; }
.zai-content { padding: 15px; }
#zai-count { font-size: 18px; font-weight: bold; color: #00ff88; margin-bottom: 5px; }
#zai-status { font-size: 10px; color: #aaa; margin-bottom: 15px; }
.zai-actions { display: flex; gap: 8px; }
.zai-actions button { flex: 1; padding: 10px 5px; font-size: 11px; border-radius: 4px; border: 1px solid #444; background: #222; color: #fff; }
.zai-actions button.primary { background: #00ff88; color: #000; font-weight: bold; border: none; }
`);
// Eventos de Toque (Mobile)
document.getElementById('zai-minimized').onclick = toggleUI;
document.getElementById('zai-close-btn').onclick = toggleUI;
document.getElementById('zai-copy').onclick = copyData;
document.getElementById('zai-save').onclick = saveData;
}
function toggleUI() {
STATE.isExpanded = !STATE.isExpanded;
document.getElementById('zai-minimized').style.display = STATE.isExpanded ? 'none' : 'flex';
document.getElementById('zai-expanded').style.display = STATE.isExpanded ? 'block' : 'none';
}
// ==========================================
// Módulo 2: Motor de Captura Heurística
// ==========================================
// Esta função não depende de classes fixas, ela busca por estruturas de mensagens
function smartCapture() {
// Busca por elementos que costumam conter mensagens em chats de IA
// chat.z.ai geralmente usa tags de markdown ou divs com pre-wrap
const messageElements = document.querySelectorAll('div[class*="message"], div[class*="content"], .markdown-body');
let sessionData = [];
messageElements.forEach((el) => {
const text = el.innerText.trim();
if (text.length < 2) return; // Ignora ruído
// Evita duplicatas na mesma sessão de scan
if (!sessionData.includes(text)) {
sessionData.push(text);
}
});
// Se detectou mudança no volume de dados, atualiza o estado
if (sessionData.length > STATE.lastCaptureCount) {
STATE.data = sessionData.map((t, i) => ({
id: i,
timestamp: new Date().toISOString(),
content: t,
platform: "chat.z.ai-mobile"
}));
STATE.lastCaptureCount = sessionData.length;
updateUI();
}
}
function updateUI() {
const el = document.getElementById('zai-count');
if (el) el.innerText = `Itens: ${STATE.data.length}`;
}
function copyData() {
GM_setClipboard(JSON.stringify(STATE.data, null, 2));
alert("Dados copiados!");
}
function saveData() {
const blob = new Blob([JSON.stringify(STATE.data, null, 2)], {type: "application/json"});
saveAs(blob, `Zai_Mobile_Chat_${Date.now()}.json`);
}
// ==========================================
// Módulo 3: Validação e Ciclo de Vida
// ==========================================
function init() {
console.log("[Z.ai] Inicializando Coletor Mobile...");
injectMobileUI();
// Observer de alta sensibilidade para Mobile
const observer = new MutationObserver(() => {
// Debounce simples para não sobrecarregar o processador do celular
clearTimeout(window.zaiTimeout);
window.zaiTimeout = setTimeout(smartCapture, 1000);
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
}
// Garante execução no chat.z.ai
if (location.hostname.includes('z.ai')) {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment