您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds "Dump chat to file" to the JanitorAI menu; exports full chat
// ==UserScript== // @name JanitorAI – Dump chat to file // @namespace gboy.jai.export // @version 2.0.0 // @description Adds "Dump chat to file" to the JanitorAI menu; exports full chat // @author Gboy // @match http*://janitorai.com/* // @match http*://www.janitorai.com/* // @match http*://*.janitorai.com/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; //SPA stuff idk it works const onRoute = (() => { const subs = new Set(); const fire = () => subs.forEach(fn => { try { fn(); } catch (e) { console.error('[jai]', e); } }); const wrap = k => { const orig = history[k]; history[k] = function (...args) { const rv = orig.apply(this, args); window.dispatchEvent(new Event('jai:route')); return rv; }; }; wrap('pushState'); wrap('replaceState'); window.addEventListener('popstate', () => window.dispatchEvent(new Event('jai:route'))); window.addEventListener('jai:route', fire); return fn => subs.add(fn); })(); const isChatPage = () => /\/chats\/\d+/.test(location.pathname); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } onRoute(() => { if (isChatPage()) ensureMenuInjector(true); }); function boot() { if (!isChatPage()) return; ensureMenuInjector(); console.debug('[jai] userscript active', location.href); } //insert to menu function ensureMenuInjector(force = false) { if (ensureMenuInjector._ok && !force) return; ensureMenuInjector._ok = true; const ICON_SVG = ` <svg stroke="currentColor" fill="currentColor" viewBox="0 0 493.525 493.525" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> <path d="M246.763,273.005c8.441,0,15.562-3.01,21.366-9.042l95.22-95.22c5.521-5.52,8.277-11.896,8.277-19.126 c0-7.23-2.759-13.419-8.277-18.559c-5.331-5.14-11.516-7.708-18.555-7.708c-7.043,0-13.229,2.568-18.559,7.708l-47.391,47.391 V18.271c0-7.043-2.52-13.229-7.566-18.559C266.231,0.381,260.189,0,252.858,0c-7.332,0-13.419,0.381-18.274,1.152 c-4.855,0.762-9.137,2.952-12.847,6.567c-3.72,3.613-5.57,8.231-5.57,13.846v161.178l-47.391-47.391 c-5.52-5.14-11.706-7.708-18.556-7.708c-7.043,0-13.229,2.568-18.559,7.708c-5.52,5.14-8.277,11.329-8.277,18.559 c0,7.23,2.759,13.606,8.277,19.126l95.225,95.22C231.387,269.995,238.511,273.005,246.763,273.005z"/> <path d="M473.086,311.218c-13.033-12.936-29.215-19.406-48.537-19.406H68.97c-19.32,0-35.5,6.471-48.537,19.406 C7.403,324.149,0,340.333,0,359.66v73.091c0,19.318,7.403,35.5,20.433,48.533c13.037,12.939,29.217,19.404,48.537,19.404h355.579 c19.322,0,35.503-6.465,48.537-19.404c13.037-13.033,20.439-29.215,20.439-48.533V359.66 C493.525,340.333,486.123,324.149,473.086,311.218z M430.518,432.751c0,5.492-2.054,10.543-6.169,15.153 c-4.109,4.61-9.092,6.912-14.935,6.912H84.1c-5.851,0-10.833-2.302-14.949-6.912c-4.117-4.61-6.171-9.661-6.171-15.153v-63.333 c0-5.478,2.054-10.521,6.171-15.133c4.116-4.61,9.099-6.916,14.949-6.916h325.313c5.843,0,10.826,2.305,14.935,6.916 c4.115,4.612,6.169,9.655,6.169,15.133V432.751z"/> </svg>`.replace(/\s+/g, ' '); //just the icon const MENU_LIST_SEL = '._menuList_162rw_8._open_162rw_22, ._menuList_162rw_8._open_'; const TEXT_STREAM_ROW_SEL = '._menuItemSwitch_hs488_94'; const MENU_ITEM_CLASS = '_menuItem_162rw_45'; const mo = new MutationObserver(() => { document.querySelectorAll(MENU_LIST_SEL).forEach(menu => { if (menu.__jai_dump_added) return; const textRow = menu.querySelector(TEXT_STREAM_ROW_SEL); if (!textRow) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = MENU_ITEM_CLASS; btn.style.display = 'flex'; btn.style.width = '100%'; btn.innerHTML = ` <span class="_menuItemIcon_162rw_81">${ICON_SVG}</span> <span class="_menuItemContent_162rw_96">Dump chat to file</span> `; btn.addEventListener('click', () => exportChat().catch(err => { console.error('[jai] export error', err); alert('Export failed: ' + (err?.message || err)); })); textRow.insertAdjacentElement('afterend', btn); menu.__jai_dump_added = true; }); }); mo.observe(document.body, { childList: true, subtree: true }); window.__jai_menuMO = mo; } //hud/userdisplay function hud() { if (hud.el) return hud; const el = document.createElement('div'); el.style.cssText = ` position:fixed; z-index:999999; right:8px; bottom:8px; font:12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; background:#111C; color:#fff; padding:8px 10px; border:1px solid #444; border-radius:10px; backdrop-filter: blur(3px); max-width: 40vw; pointer-events:none; white-space:pre-wrap; `; document.documentElement.appendChild(el); hud.el = el; hud.tick = (obj) => { el.textContent = `[JAI Export]\n${Object.entries(obj).map(([k,v])=>`${k}: ${v}`).join('\n')}`; }; hud.done = (msg='Done') => { el.textContent += `\n${msg}`; setTimeout(()=>el.remove(), 3000); hud.el = null; }; return hud; } //util funcs const sleep = ms => new Promise(r => setTimeout(r, ms)); const sel = s => document.querySelector(s); const selAll = s => Array.from(document.querySelectorAll(s)); function getScroller() { return document.querySelector('[data-testid="virtuoso-scroller"][data-virtuoso-scroller="true"]'); } function getItemList() { return document.querySelector('[data-testid="virtuoso-item-list"]'); } function getAllItems() { const list = getItemList(); return list ? Array.from(list.querySelectorAll('[data-index][data-item-index]')) : []; } function filenameFromHeader() { const who = document.querySelector('._customDivider_7bh05_1 ._customDividerText_7bh05_26'); const name = (who?.textContent?.trim() || 'JanitorAI'); const id = location.pathname.split('/').pop() || 'chat'; const stamp = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); return `${name}_${id}_${stamp}.txt`; } //viewport function applyViewportBoost(scroller, { enableZoom = true, zoomScale = 0.8, heightVH = 500 } = {}) { const prev = { bodyZoom: document.body.style.zoom || '', scrHeight: scroller.style.height || '', htmlScrollBehavior: document.documentElement.style.scrollBehavior || '', }; if (enableZoom) document.body.style.zoom = String(zoomScale); // emulate Cmd/- scroller.style.height = `${heightVH}vh`; // load more items per sweep document.documentElement.style.scrollBehavior = 'auto'; // avoid “pendulum” at edges return () => { document.body.style.zoom = prev.bodyZoom; scroller.style.height = prev.scrHeight; document.documentElement.style.scrollBehavior = prev.htmlScrollBehavior; }; } //sweep async function sweepAll({ dir = 'down', stepPx = 1600, idleLimit = 12, stallJiggle = 3, maxMs = 180000 } = {}) { const H = hud(); H.tick({ phase: 'sweep init', dir, stepPx, idleLimit }); const scr = getScroller(); if (!scr) throw new Error('Scroller not found'); const off = applyViewportBoost(scr, { enableZoom: true, zoomScale: 0.8, heightVH: 500 }); const start = performance.now(); let lastMax = -1, idle = 0, totalSteps = 0; const seen = new Set(); const getMaxIndex = () => { let max = -1; getAllItems().forEach(it => { const idx = Number(it.getAttribute('data-index')); if (!Number.isNaN(idx)) max = Math.max(max, idx); if (it.dataset._jai_seen !== '1') { it.dataset._jai_seen = '1'; seen.add(idx); } }); return max; }; await sleep(50); lastMax = getMaxIndex(); while (true) { if ((performance.now() - start) > maxMs) { H.tick({ phase:'sweep timeout', totalSteps, maxIndex:lastMax, seen:seen.size }); break; } totalSteps++; const nextTop = scr.scrollTop + (dir === 'down' ? stepPx : -stepPx); scr.scrollTo({ top: Math.max(0, nextTop) }); await sleep(50); const maxIdx = getMaxIndex(); if (maxIdx > lastMax) { idle = 0; lastMax = maxIdx; } else { idle++; if (idle % stallJiggle === 0) { scr.scrollBy({ top: -300 }); await sleep(30); scr.scrollBy({ top: +600 }); await sleep(30); } } const atBottom = Math.abs(scr.scrollHeight - (scr.scrollTop + scr.clientHeight)) < 4; const atTop = scr.scrollTop <= 2; H.tick({ phase:'sweeping', step: totalSteps, maxIndex: lastMax, idle, jiggles: Math.floor(idle/stallJiggle), atBottom, atTop, seen: seen.size }); if ((dir === 'down' && atBottom && idle >= idleLimit) || (dir === 'up' && atTop && idle >= idleLimit)) { H.tick({ phase:'sweep complete', steps: totalSteps, maxIndex:lastMax, seen: seen.size }); break; } } off(); return { steps: totalSteps, maxIndex: lastMax, distinct: seen.size }; } //extraction function extractMessages() { const items = getAllItems(); const out = []; for (const it of items) { const body = it.querySelector('._messageBody_cj98i_56'); if (!body) continue; const nameNode = body.querySelector('._nameContainer_prxth_282 ._nameText_prxth_288'); const name = (nameNode?.textContent || 'Unknown').trim(); const contentRoot = body.querySelector('.css-ji4crq'); if (!contentRoot) continue; const chunks = []; contentRoot.querySelectorAll(':scope > .css-0 > div').forEach(div => { const t = div.textContent; if (t && t.trim().length) chunks.push(t.replace(/\u00A0/g, ' ').trim()); }); let text = chunks.join('\n\n').trim(); if (!text) continue; const header = `[${name}]:`; out.push(`${header}\n${text}\n`); } return out; } //export entry async function exportChat() { const H = hud(); H.tick({ phase: 'start' }); await sweepAll({ dir:'down', stepPx: 1800, idleLimit: 10, stallJiggle: 3, maxMs: 180000 }); await sleep(80); await sweepAll({ dir:'up', stepPx: 1400, idleLimit: 8, stallJiggle: 2, maxMs: 60000 }); const msgs = extractMessages(); if (!msgs.length) throw new Error('No messages extracted'); const header = `URL: ${location.href}\nExported: ${new Date().toString()}\nMessages: ${msgs.length}\n---\n\n`; const blob = new Blob([header + msgs.join('\n')], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filenameFromHeader(); document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 2000); H.done('Saved.'); } })();