|
(async () => { |
|
/** |
|
* AI Chat History Cleaner - Gemini / Copilot / Claude / Meta AI |
|
* |
|
* Supported: |
|
* - Google Gemini: gemini.google.com |
|
* - Microsoft Copilot: copilot.microsoft.com |
|
* - Claude: claude.ai |
|
* - Meta AI: meta.ai |
|
* |
|
* Stop: |
|
* window.__aiChatHistoryCleanerStop = true; |
|
*/ |
|
|
|
const CONFIG = { |
|
defaultLimit: 20, |
|
maxLimit: 500, |
|
|
|
delayAfterMenuOpen: 850, |
|
delayAfterDeleteClick: 1000, |
|
delayAfterConfirmClick: 2600, |
|
delayAfterNoConfirmDelete: 2200, |
|
|
|
requireFinalConfirmation: true, |
|
|
|
enableLazyPreload: true, |
|
preloadExtraBuffer: 10, |
|
preloadScrollDelay: 1200, |
|
preloadMaxRounds: 90, |
|
preloadStableLimit: 5, |
|
lowWatermarkRows: 5, |
|
|
|
retryLoadMoreRounds: 12, |
|
}; |
|
|
|
window.__aiChatHistoryCleanerStop = false; |
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); |
|
|
|
const unique = (array) => |
|
array.filter((item, index, arr) => item && arr.indexOf(item) === index); |
|
|
|
const getText = (element) => { |
|
return ( |
|
( |
|
element?.innerText || |
|
element?.textContent || |
|
element?.getAttribute?.("aria-label") || |
|
element?.getAttribute?.("title") || |
|
"" |
|
) + "" |
|
) |
|
.replace(/\s+/g, " ") |
|
.trim(); |
|
}; |
|
|
|
const isVisible = (element) => { |
|
if (!element) return false; |
|
|
|
const rect = element.getBoundingClientRect(); |
|
const style = getComputedStyle(element); |
|
|
|
return ( |
|
rect.width > 0 && |
|
rect.height > 0 && |
|
rect.bottom > 0 && |
|
rect.top < window.innerHeight && |
|
rect.right > 0 && |
|
rect.left < window.innerWidth && |
|
style.display !== "none" && |
|
style.visibility !== "hidden" && |
|
style.opacity !== "0" |
|
); |
|
}; |
|
|
|
const isUsableElement = (element) => { |
|
if (!element) return false; |
|
|
|
const style = getComputedStyle(element); |
|
|
|
return style.display !== "none" && style.visibility !== "hidden"; |
|
}; |
|
|
|
const getClickableElement = (element) => { |
|
if (!element) return null; |
|
|
|
return ( |
|
element.closest?.( |
|
[ |
|
"button", |
|
'[role="button"]', |
|
'[role="menuitem"]', |
|
'[data-slot="dropdown-menu-item"]', |
|
'[data-testid="delete-chat-trigger"]', |
|
".mat-mdc-menu-item", |
|
"a", |
|
].join(", ") |
|
) || element |
|
); |
|
}; |
|
|
|
const dispatchHover = (element) => { |
|
if (!element) return; |
|
|
|
const rect = element.getBoundingClientRect(); |
|
|
|
const options = { |
|
bubbles: true, |
|
cancelable: true, |
|
composed: true, |
|
view: window, |
|
clientX: rect.right - 20, |
|
clientY: rect.top + rect.height / 2, |
|
}; |
|
|
|
[ |
|
"pointerover", |
|
"pointerenter", |
|
"pointermove", |
|
"mouseover", |
|
"mouseenter", |
|
"mousemove", |
|
].forEach((eventName) => { |
|
try { |
|
const EventClass = |
|
eventName.startsWith("pointer") && window.PointerEvent |
|
? PointerEvent |
|
: MouseEvent; |
|
|
|
element.dispatchEvent(new EventClass(eventName, options)); |
|
} catch (_) { |
|
try { |
|
element.dispatchEvent(new MouseEvent(eventName, options)); |
|
} catch (_) {} |
|
} |
|
}); |
|
}; |
|
|
|
const clickElement = async (rawElement) => { |
|
const element = getClickableElement(rawElement); |
|
|
|
if (!element) return false; |
|
|
|
element.scrollIntoView({ block: "center", inline: "center" }); |
|
await sleep(150); |
|
|
|
try { |
|
element.focus?.({ preventScroll: true }); |
|
} catch (_) { |
|
try { |
|
element.focus?.(); |
|
} catch (_) {} |
|
} |
|
|
|
const rect = element.getBoundingClientRect(); |
|
|
|
const options = { |
|
bubbles: true, |
|
cancelable: true, |
|
composed: true, |
|
view: window, |
|
clientX: rect.left + rect.width / 2, |
|
clientY: rect.top + rect.height / 2, |
|
}; |
|
|
|
const events = [ |
|
"pointerover", |
|
"pointerenter", |
|
"pointermove", |
|
"mouseover", |
|
"mouseenter", |
|
"mousemove", |
|
"pointerdown", |
|
"mousedown", |
|
"pointerup", |
|
"mouseup", |
|
"click", |
|
]; |
|
|
|
for (const eventName of events) { |
|
try { |
|
const EventClass = |
|
eventName.startsWith("pointer") && window.PointerEvent |
|
? PointerEvent |
|
: MouseEvent; |
|
|
|
element.dispatchEvent(new EventClass(eventName, options)); |
|
} catch (_) { |
|
try { |
|
element.dispatchEvent(new MouseEvent(eventName, options)); |
|
} catch (_) {} |
|
} |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
const dispatchScroll = (element) => { |
|
if (!element) return; |
|
|
|
try { |
|
element.dispatchEvent(new Event("scroll", { bubbles: true })); |
|
} catch (_) {} |
|
|
|
try { |
|
element.dispatchEvent( |
|
new WheelEvent("wheel", { |
|
bubbles: true, |
|
cancelable: true, |
|
deltaY: 1200, |
|
}) |
|
); |
|
} catch (_) { |
|
try { |
|
element.dispatchEvent(new Event("wheel", { bubbles: true })); |
|
} catch (_) {} |
|
} |
|
}; |
|
|
|
const forceScrollBottom = (element) => { |
|
if (!element) return; |
|
|
|
try { |
|
element.scrollTop = element.scrollHeight; |
|
dispatchScroll(element); |
|
} catch (_) {} |
|
|
|
try { |
|
element.scrollTo({ |
|
top: element.scrollHeight, |
|
behavior: "instant", |
|
}); |
|
dispatchScroll(element); |
|
} catch (_) {} |
|
}; |
|
|
|
const forceScrollTop = (element) => { |
|
if (!element) return; |
|
|
|
try { |
|
element.scrollTop = 0; |
|
dispatchScroll(element); |
|
} catch (_) {} |
|
|
|
try { |
|
element.scrollTo({ |
|
top: 0, |
|
behavior: "instant", |
|
}); |
|
dispatchScroll(element); |
|
} catch (_) {} |
|
}; |
|
|
|
const findBestScrollContainer = (root) => { |
|
const candidates = []; |
|
|
|
let node = root; |
|
while (node && node !== document.documentElement) { |
|
candidates.push(node); |
|
node = node.parentElement; |
|
} |
|
|
|
if (root?.querySelectorAll) { |
|
candidates.push(...root.querySelectorAll("*")); |
|
} |
|
|
|
candidates.push( |
|
document.querySelector("aside"), |
|
document.querySelector("nav"), |
|
document.scrollingElement, |
|
document.documentElement, |
|
document.body |
|
); |
|
|
|
const scored = unique(candidates) |
|
.filter(Boolean) |
|
.map((el) => { |
|
const rect = el.getBoundingClientRect?.() || { |
|
left: 0, |
|
top: 0, |
|
width: window.innerWidth, |
|
height: window.innerHeight, |
|
}; |
|
|
|
const style = getComputedStyle(el); |
|
const scrollableAmount = (el.scrollHeight || 0) - (el.clientHeight || 0); |
|
|
|
let score = 0; |
|
|
|
if (scrollableAmount > 40) score += 100; |
|
if (/auto|scroll/i.test(style.overflowY)) score += 45; |
|
if (rect.left < 700) score += 35; |
|
if (rect.width > 160 && rect.width < 750) score += 35; |
|
if (rect.height > 250) score += 20; |
|
if (el.matches?.("aside, nav")) score += 20; |
|
score += Math.min(120, scrollableAmount / 20); |
|
|
|
return { |
|
el, |
|
score, |
|
scrollableAmount, |
|
rect, |
|
overflowY: style.overflowY, |
|
}; |
|
}) |
|
.filter((item) => item.scrollableAmount > 40) |
|
.sort((a, b) => b.score - a.score); |
|
|
|
return scored[0]?.el || document.scrollingElement || document.documentElement; |
|
}; |
|
|
|
const getMenuRoots = () => { |
|
return [ |
|
...document.querySelectorAll('[role="menu"]'), |
|
...document.querySelectorAll('[data-cds="Menu"]'), |
|
...document.querySelectorAll('[data-base-ui-focusable][role="menu"]'), |
|
...document.querySelectorAll('[data-floating-ui-focusable][role="menu"]'), |
|
...document.querySelectorAll('[data-radix-popper-content-wrapper]'), |
|
...document.querySelectorAll('[data-radix-menu-content]'), |
|
...document.querySelectorAll('[data-slot*="dropdown"]'), |
|
...document.querySelectorAll('[data-sidebar*="menu"]'), |
|
...document.querySelectorAll(".cdk-overlay-container"), |
|
...document.querySelectorAll("[data-testid*='menu']"), |
|
...document.querySelectorAll("[id^='radix-']"), |
|
...document.querySelectorAll("[id^='_r_']"), |
|
document.body, |
|
].filter(Boolean); |
|
}; |
|
|
|
const findDeleteMenuItem = async (serviceName) => { |
|
for (let attempt = 0; attempt < 20; attempt++) { |
|
const roots = getMenuRoots(); |
|
|
|
for (const root of roots) { |
|
const candidates = [ |
|
...root.querySelectorAll( |
|
[ |
|
// Claude 확정 셀렉터 |
|
'[data-testid="delete-chat-trigger"]', |
|
|
|
// Meta 확정 계열 |
|
'[data-radix-popper-content-wrapper] [role="menuitem"][data-slot="dropdown-menu-item"]', |
|
'[data-radix-menu-content] [role="menuitem"][data-slot="dropdown-menu-item"]', |
|
|
|
// 범용 메뉴 항목 |
|
'button[role="menuitem"]', |
|
'[role="menuitem"]', |
|
'[data-slot="dropdown-menu-item"]', |
|
'[data-radix-collection-item]', |
|
|
|
// 텍스트/속성 기반 |
|
'button[title*="대화 삭제"]', |
|
'button[aria-label*="대화 삭제"]', |
|
'button[title*="채팅 삭제"]', |
|
'button[aria-label*="채팅 삭제"]', |
|
'button[title*="삭제"]', |
|
'button[aria-label*="삭제"]', |
|
'button[title*="Delete"]', |
|
'button[aria-label*="Delete"]', |
|
'button[title*="Remove"]', |
|
'button[aria-label*="Remove"]', |
|
|
|
".mat-mdc-menu-item", |
|
"button", |
|
"div", |
|
"span", |
|
].join(", ") |
|
), |
|
].filter(isVisible); |
|
|
|
const scored = candidates |
|
.map((element) => { |
|
const clickable = getClickableElement(element); |
|
const text = getText(element); |
|
const clickableText = getText(clickable); |
|
|
|
const title = |
|
element.getAttribute?.("title") || |
|
clickable?.getAttribute?.("title") || |
|
""; |
|
|
|
const aria = |
|
element.getAttribute?.("aria-label") || |
|
clickable?.getAttribute?.("aria-label") || |
|
""; |
|
|
|
const testId = |
|
element.getAttribute?.("data-testid") || |
|
clickable?.getAttribute?.("data-testid") || |
|
""; |
|
|
|
const slot = |
|
element.getAttribute?.("data-slot") || |
|
clickable?.getAttribute?.("data-slot") || |
|
""; |
|
|
|
const className = |
|
element.getAttribute?.("class") || |
|
clickable?.getAttribute?.("class") || |
|
""; |
|
|
|
const combined = `${text} ${clickableText} ${title} ${aria} ${testId} ${slot} ${className}` |
|
.replace(/\s+/g, " ") |
|
.trim(); |
|
|
|
let score = 0; |
|
|
|
// Claude 확정 |
|
if (/delete-chat-trigger/i.test(testId)) { |
|
score += 280; |
|
} |
|
|
|
// Meta 확정: Radix dropdown menu item + destructive class + 삭제 |
|
if ( |
|
serviceName === "Meta AI" && |
|
/dropdown-menu-item/i.test(slot) && |
|
/text-text-destructive/i.test(className) && |
|
/삭제/.test(combined) |
|
) { |
|
score += 300; |
|
} |
|
|
|
if ( |
|
serviceName === "Meta AI" && |
|
element.closest?.("[data-radix-popper-content-wrapper]") && |
|
/text-text-destructive/i.test(className) && |
|
/삭제/.test(combined) |
|
) { |
|
score += 300; |
|
} |
|
|
|
// 한국어 삭제 문구 |
|
if (/대화 삭제|채팅 삭제|삭제.*대화|삭제.*채팅/i.test(combined)) { |
|
score += 160; |
|
} |
|
|
|
// 영어 삭제 문구 |
|
if ( |
|
/Delete conversation|Delete chat|Delete prompt|Remove conversation|Remove chat/i.test( |
|
combined |
|
) |
|
) { |
|
score += 160; |
|
} |
|
|
|
// 단독 삭제 텍스트 |
|
if (/^삭제$|^삭제하기$|^Delete$|^Remove$/i.test(text)) { |
|
score += 130; |
|
} |
|
|
|
if (/^삭제$|^삭제하기$|^Delete$|^Remove$/i.test(clickableText)) { |
|
score += 130; |
|
} |
|
|
|
if (/delete|trash|remove/i.test(combined)) { |
|
score += 70; |
|
} |
|
|
|
// 위험/파괴 클래스 가산점 |
|
if (/danger|destructive|red|text-text-destructive|text-danger|group-menu-danger/i.test(className)) { |
|
score += 80; |
|
} |
|
|
|
// 역할/태그 가산점 |
|
if (clickable?.matches?.('button[role="menuitem"]')) score += 50; |
|
if (clickable?.matches?.('[role="menuitem"]')) score += 45; |
|
if (clickable?.matches?.('[data-slot="dropdown-menu-item"]')) score += 45; |
|
if (clickable?.matches?.(".mat-mdc-menu-item")) score += 35; |
|
if (clickable?.tagName?.toLowerCase() === "button") score += 25; |
|
|
|
// 삭제가 아닌 항목 감점 |
|
if ( |
|
/즐겨찾기|고정|Pin|Star|이름 변경|이름 바꾸기|Rename|초대|Invite|공유|Share|복사|Copy|요약|Summary|프로젝트|Project|보고|Report|Archive|보관|Export|내보내기|Move|이동|추가|Add/i.test( |
|
combined |
|
) |
|
) { |
|
score -= 220; |
|
} |
|
|
|
return { |
|
rawElement: element, |
|
element: clickable, |
|
score, |
|
text, |
|
clickableText, |
|
title, |
|
aria, |
|
testId, |
|
slot, |
|
className, |
|
combined, |
|
}; |
|
}) |
|
.filter((item) => item.element && item.score >= 110) |
|
.sort((a, b) => b.score - a.score); |
|
|
|
if (scored.length > 0) { |
|
const picked = scored[0]; |
|
|
|
console.log("[AI Chat Cleaner] 삭제 메뉴 감지:", { |
|
service: serviceName, |
|
text: picked.text, |
|
clickableText: picked.clickableText, |
|
title: picked.title, |
|
aria: picked.aria, |
|
testId: picked.testId, |
|
slot: picked.slot, |
|
score: picked.score, |
|
}); |
|
|
|
return picked.element; |
|
} |
|
} |
|
|
|
await sleep(200); |
|
} |
|
|
|
return null; |
|
}; |
|
|
|
const findConfirmDeleteButton = async (serviceName) => { |
|
for (let attempt = 0; attempt < 24; attempt++) { |
|
const roots = [ |
|
...document.querySelectorAll('[role="dialog"]'), |
|
...document.querySelectorAll('[role="alertdialog"]'), |
|
...document.querySelectorAll("mat-dialog-container"), |
|
...document.querySelectorAll('[data-testid*="dialog"]'), |
|
...document.querySelectorAll('[data-slot="dialog-content"]'), |
|
...document.querySelectorAll('[data-floating-ui-focusable]'), |
|
...document.querySelectorAll('[data-radix-popper-content-wrapper]'), |
|
...document.querySelectorAll('[data-radix-dialog-content]'), |
|
...document.querySelectorAll(".cdk-overlay-container"), |
|
document.body, |
|
].filter(Boolean); |
|
|
|
for (const root of roots) { |
|
const rootText = getText(root); |
|
|
|
const buttons = [ |
|
...root.querySelectorAll('button, [role="button"], [data-slot="button"]'), |
|
].filter(isVisible); |
|
|
|
const scored = buttons |
|
.map((button) => { |
|
const text = getText(button); |
|
const title = button.getAttribute?.("title") || ""; |
|
const aria = button.getAttribute?.("aria-label") || ""; |
|
const className = button.getAttribute?.("class") || ""; |
|
|
|
const combined = `${text} ${title} ${aria} ${rootText} ${className}` |
|
.replace(/\s+/g, " ") |
|
.trim(); |
|
|
|
let score = 0; |
|
|
|
// Claude 확인창: "대화 삭제" + "삭제" |
|
if (/대화 삭제/i.test(rootText) && /^삭제$/i.test(text)) { |
|
score += 280; |
|
} |
|
|
|
// Meta 확인창: "채팅을 삭제할까요?" + "삭제" |
|
if (/채팅을 삭제할까요/i.test(rootText) && /^삭제$/i.test(text)) { |
|
score += 280; |
|
} |
|
|
|
// 범용 확인창 |
|
if (/^삭제$|^삭제하기$|^Delete$|^Remove$/i.test(text)) score += 150; |
|
if (/대화 삭제|채팅 삭제|삭제.*대화|삭제.*채팅/i.test(combined)) score += 120; |
|
|
|
if ( |
|
/Delete conversation|Delete chat|Delete prompt|Remove conversation|Remove chat/i.test( |
|
combined |
|
) |
|
) { |
|
score += 120; |
|
} |
|
|
|
if (/확인|Confirm|OK/i.test(text)) score += 25; |
|
|
|
// 위험 버튼 클래스 가산점 |
|
if ( |
|
/danger|red|accent|destructive|text-on-danger|text-text-on-accent|gradient-red|from-gradient-red/i.test( |
|
className |
|
) |
|
) { |
|
score += 60; |
|
} |
|
|
|
// 취소/닫기 감점 |
|
if (/취소|Cancel|닫기|Close/i.test(text)) score -= 450; |
|
if (/닫기/i.test(aria)) score -= 450; |
|
|
|
return { |
|
button, |
|
score, |
|
text, |
|
title, |
|
aria, |
|
combined, |
|
}; |
|
}) |
|
.filter((item) => item.score >= 120) |
|
.sort((a, b) => b.score - a.score); |
|
|
|
if (scored.length > 0) { |
|
const picked = scored[0]; |
|
|
|
console.log("[AI Chat Cleaner] 삭제 확인 버튼 감지:", { |
|
service: serviceName, |
|
text: picked.text, |
|
title: picked.title, |
|
aria: picked.aria, |
|
score: picked.score, |
|
}); |
|
|
|
return picked.button; |
|
} |
|
} |
|
|
|
await sleep(200); |
|
} |
|
|
|
return null; |
|
}; |
|
|
|
const waitForRowCountChange = async ( |
|
cleaner, |
|
beforeCount, |
|
targetKey, |
|
timeoutMs = 5000 |
|
) => { |
|
const start = Date.now(); |
|
|
|
while (Date.now() - start < timeoutMs) { |
|
const rows = cleaner.getRows(); |
|
const currentCount = rows.length; |
|
const stillExists = targetKey |
|
? rows.some((row) => cleaner.getKey(row) === targetKey) |
|
: false; |
|
|
|
if (currentCount < beforeCount || !stillExists) { |
|
return true; |
|
} |
|
|
|
await sleep(250); |
|
} |
|
|
|
return false; |
|
}; |
|
|
|
const preloadRowsForCleaner = async (cleaner, targetCount) => { |
|
if (!CONFIG.enableLazyPreload) return; |
|
if (!cleaner.supportsLazyLoad) return; |
|
|
|
const desiredCount = Math.min( |
|
targetCount + CONFIG.preloadExtraBuffer, |
|
CONFIG.maxLimit + CONFIG.preloadExtraBuffer |
|
); |
|
|
|
const scroller = cleaner.getScrollContainer?.(); |
|
|
|
if (!scroller) { |
|
console.warn(`[AI Chat Cleaner] ${cleaner.name} 스크롤 컨테이너를 찾지 못했습니다.`); |
|
return; |
|
} |
|
|
|
let previousCount = cleaner.getRows().length; |
|
let stableRounds = 0; |
|
|
|
console.log(`[AI Chat Cleaner] ${cleaner.name} 대화 목록 사전 로딩 시작:`, { |
|
현재로드수: previousCount, |
|
목표로드수: desiredCount, |
|
scrollHeight: scroller.scrollHeight, |
|
clientHeight: scroller.clientHeight, |
|
}); |
|
|
|
for (let round = 1; round <= CONFIG.preloadMaxRounds; round++) { |
|
if (window.__aiChatHistoryCleanerStop) { |
|
console.warn("[AI Chat Cleaner] 중지 요청으로 사전 로딩을 멈췄습니다."); |
|
break; |
|
} |
|
|
|
const currentCount = cleaner.getRows().length; |
|
|
|
if (currentCount >= desiredCount) { |
|
console.log(`[AI Chat Cleaner] ${cleaner.name} 사전 로딩 목표 도달:`, { |
|
로드수: currentCount, |
|
목표: desiredCount, |
|
}); |
|
break; |
|
} |
|
|
|
forceScrollBottom(scroller); |
|
await sleep(CONFIG.preloadScrollDelay); |
|
|
|
const newCount = cleaner.getRows().length; |
|
|
|
console.log(`[AI Chat Cleaner] ${cleaner.name} 사전 로딩 진행:`, { |
|
round, |
|
이전로드수: currentCount, |
|
현재로드수: newCount, |
|
목표로드수: desiredCount, |
|
}); |
|
|
|
if (newCount <= previousCount) { |
|
stableRounds += 1; |
|
} else { |
|
stableRounds = 0; |
|
previousCount = newCount; |
|
} |
|
|
|
if (stableRounds >= CONFIG.preloadStableLimit) { |
|
console.log( |
|
`[AI Chat Cleaner] ${cleaner.name} 추가 로드 증가가 멈춰 사전 로딩을 종료합니다.`, |
|
{ |
|
로드수: newCount, |
|
stableRounds, |
|
} |
|
); |
|
break; |
|
} |
|
} |
|
|
|
forceScrollTop(scroller); |
|
await sleep(700); |
|
|
|
console.log(`[AI Chat Cleaner] ${cleaner.name} 대화 목록 사전 로딩 종료:`, { |
|
최종로드수: cleaner.getRows().length, |
|
}); |
|
}; |
|
|
|
const loadMoreIfNeeded = async (cleaner, remainingTarget) => { |
|
if (!cleaner.supportsLazyLoad) return; |
|
|
|
const rows = cleaner.getRows(); |
|
|
|
if (rows.length > CONFIG.lowWatermarkRows) return; |
|
|
|
const scroller = cleaner.getScrollContainer?.(); |
|
|
|
if (!scroller) { |
|
console.warn(`[AI Chat Cleaner] ${cleaner.name} 스크롤 컨테이너를 찾지 못했습니다.`); |
|
return; |
|
} |
|
|
|
let previousCount = rows.length; |
|
let stableRounds = 0; |
|
|
|
console.log(`[AI Chat Cleaner] ${cleaner.name} 로드된 대화가 부족해 추가 로딩을 시도합니다.`, { |
|
현재로드수: previousCount, |
|
남은삭제목표: remainingTarget, |
|
}); |
|
|
|
for (let round = 1; round <= CONFIG.retryLoadMoreRounds; round++) { |
|
if (window.__aiChatHistoryCleanerStop) break; |
|
|
|
forceScrollBottom(scroller); |
|
await sleep(CONFIG.preloadScrollDelay); |
|
|
|
const newCount = cleaner.getRows().length; |
|
|
|
console.log(`[AI Chat Cleaner] ${cleaner.name} 추가 로딩 진행:`, { |
|
round, |
|
이전로드수: previousCount, |
|
현재로드수: newCount, |
|
}); |
|
|
|
if (newCount > CONFIG.lowWatermarkRows) break; |
|
|
|
if (newCount <= previousCount) { |
|
stableRounds += 1; |
|
} else { |
|
stableRounds = 0; |
|
previousCount = newCount; |
|
} |
|
|
|
if (stableRounds >= 3) break; |
|
} |
|
|
|
forceScrollTop(scroller); |
|
await sleep(500); |
|
}; |
|
|
|
const GeminiCleaner = { |
|
name: "Google Gemini", |
|
supportsLazyLoad: true, |
|
|
|
detect() { |
|
return ( |
|
location.hostname.includes("gemini.google.com") || |
|
!!document.querySelector('[data-test-id="all-conversations"]') || |
|
!!document.querySelector( |
|
'[data-test-id="conversation"] [data-test-id="actions-menu-button"]' |
|
) |
|
); |
|
}, |
|
|
|
getRoot() { |
|
return ( |
|
document.querySelector('[data-test-id="all-conversations"]') || |
|
document.querySelector("#sidenav-section-content-chats") || |
|
document.querySelector(".chat-history-list") || |
|
document.querySelector(".chat-history") || |
|
document.body |
|
); |
|
}, |
|
|
|
getScrollContainer() { |
|
return findBestScrollContainer(this.getRoot()); |
|
}, |
|
|
|
forceShowActionButtons() { |
|
if (!document.getElementById("__ai_cleaner_gemini_css")) { |
|
const style = document.createElement("style"); |
|
style.id = "__ai_cleaner_gemini_css"; |
|
style.textContent = ` |
|
[data-test-id="conversation"] .hovered-trailing-content, |
|
[data-test-id="conversation"] [data-test-id="actions-menu-button"], |
|
[data-test-id="conversation"] [data-test-id="actions-menu-button"] button { |
|
display: flex !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
} |
|
|
|
[data-test-id="conversation"] .trailing-content { |
|
opacity: 0 !important; |
|
pointer-events: none !important; |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
}, |
|
|
|
getRows() { |
|
const root = this.getRoot(); |
|
|
|
return [...root.querySelectorAll('[data-test-id="conversation"]')] |
|
.filter(isUsableElement) |
|
.filter((row) => row.querySelector('a[href^="/app/"]')) |
|
.sort( |
|
(a, b) => |
|
a.getBoundingClientRect().top - b.getBoundingClientRect().top |
|
); |
|
}, |
|
|
|
getTitle(row) { |
|
const titleElement = row.querySelector(".title-text"); |
|
const linkElement = row.querySelector("a[aria-label]"); |
|
|
|
return ( |
|
getText(titleElement) || |
|
linkElement?.getAttribute("aria-label") || |
|
getText(row) || |
|
"(제목 없음)" |
|
); |
|
}, |
|
|
|
getKey(row) { |
|
const linkElement = row.querySelector('a[href^="/app/"]'); |
|
return linkElement?.getAttribute("href") || this.getTitle(row); |
|
}, |
|
|
|
async getMoreButton(row) { |
|
this.forceShowActionButtons(); |
|
|
|
row.scrollIntoView({ block: "center" }); |
|
dispatchHover(row); |
|
|
|
await sleep(300); |
|
|
|
return ( |
|
row.querySelector('[data-test-id="actions-menu-button"] button') || |
|
row.querySelector('[data-test-id="actions-menu-button"]') || |
|
row.querySelector('button[aria-label*="더보기"]') || |
|
row.querySelector('button[aria-label*="옵션"]') || |
|
row.querySelector('mat-icon[fonticon="more_vert"]')?.closest("button") || |
|
null |
|
); |
|
}, |
|
}; |
|
|
|
const CopilotCleaner = { |
|
name: "Microsoft Copilot", |
|
supportsLazyLoad: true, |
|
|
|
detect() { |
|
return ( |
|
location.hostname.includes("copilot.microsoft.com") || |
|
!!document.querySelector('[data-testid="sidebar-expanded-content"]') || |
|
!!document.querySelector('button[id^="conversation-options-"]') |
|
); |
|
}, |
|
|
|
getRoot() { |
|
return ( |
|
document.querySelector('[data-testid="sidebar-expanded-content"]') || |
|
document.querySelector('[role="list"]') || |
|
document.body |
|
); |
|
}, |
|
|
|
getScrollContainer() { |
|
return findBestScrollContainer(this.getRoot()); |
|
}, |
|
|
|
forceShowActionButtons() { |
|
if (!document.getElementById("__ai_cleaner_copilot_css")) { |
|
const style = document.createElement("style"); |
|
style.id = "__ai_cleaner_copilot_css"; |
|
style.textContent = ` |
|
button[id^="conversation-options-"] { |
|
display: flex !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
position: static !important; |
|
width: 24px !important; |
|
height: 24px !important; |
|
min-width: 24px !important; |
|
min-height: 24px !important; |
|
clip: auto !important; |
|
clip-path: none !important; |
|
overflow: visible !important; |
|
transform: none !important; |
|
} |
|
|
|
span:has(> button[id^="conversation-options-"]) { |
|
display: inline-flex !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
position: static !important; |
|
width: auto !important; |
|
height: auto !important; |
|
clip: auto !important; |
|
clip-path: none !important; |
|
overflow: visible !important; |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
|
|
document.querySelectorAll('button[id^="conversation-options-"]').forEach((button) => { |
|
const parent = button.parentElement; |
|
|
|
button.style.setProperty("display", "flex", "important"); |
|
button.style.setProperty("visibility", "visible", "important"); |
|
button.style.setProperty("opacity", "1", "important"); |
|
button.style.setProperty("pointer-events", "auto", "important"); |
|
button.style.setProperty("position", "static", "important"); |
|
button.style.setProperty("width", "24px", "important"); |
|
button.style.setProperty("height", "24px", "important"); |
|
button.style.setProperty("clip", "auto", "important"); |
|
button.style.setProperty("clip-path", "none", "important"); |
|
button.style.setProperty("overflow", "visible", "important"); |
|
|
|
if (parent) { |
|
parent.classList.remove("sr-only"); |
|
parent.style.setProperty("display", "inline-flex", "important"); |
|
parent.style.setProperty("visibility", "visible", "important"); |
|
parent.style.setProperty("opacity", "1", "important"); |
|
parent.style.setProperty("pointer-events", "auto", "important"); |
|
parent.style.setProperty("position", "static", "important"); |
|
parent.style.setProperty("width", "auto", "important"); |
|
parent.style.setProperty("height", "auto", "important"); |
|
parent.style.setProperty("clip", "auto", "important"); |
|
parent.style.setProperty("clip-path", "none", "important"); |
|
parent.style.setProperty("overflow", "visible", "important"); |
|
} |
|
}); |
|
}, |
|
|
|
getRows() { |
|
this.forceShowActionButtons(); |
|
|
|
const root = this.getRoot(); |
|
|
|
return [...root.querySelectorAll('[role="link"][aria-label]')] |
|
.filter(isUsableElement) |
|
.filter((row) => row.querySelector('button[id^="conversation-options-"]')) |
|
.filter((row) => { |
|
const title = this.getTitle(row); |
|
return !/새 대화|New chat|New conversation/i.test(title); |
|
}) |
|
.sort( |
|
(a, b) => |
|
a.getBoundingClientRect().top - b.getBoundingClientRect().top |
|
); |
|
}, |
|
|
|
getTitle(row) { |
|
const titleElement = row.querySelector("p[title]"); |
|
|
|
return ( |
|
titleElement?.getAttribute("title") || |
|
row.getAttribute("aria-label") || |
|
getText(row) || |
|
"(제목 없음)" |
|
); |
|
}, |
|
|
|
getKey(row) { |
|
const button = row.querySelector('button[id^="conversation-options-"]'); |
|
return button?.id || this.getTitle(row); |
|
}, |
|
|
|
async getMoreButton(row) { |
|
this.forceShowActionButtons(); |
|
|
|
row.scrollIntoView({ block: "center" }); |
|
dispatchHover(row); |
|
|
|
await sleep(300); |
|
|
|
return ( |
|
row.querySelector('button[id^="conversation-options-"]') || |
|
row.querySelector('button[aria-label*="보기 옵션"]') || |
|
row.querySelector('button[title*="보기 옵션"]') || |
|
row.querySelector('button[aria-label*="options" i]') || |
|
row.querySelector('button[title*="options" i]') || |
|
null |
|
); |
|
}, |
|
}; |
|
|
|
const ClaudeCleaner = { |
|
name: "Claude", |
|
supportsLazyLoad: true, |
|
|
|
detect() { |
|
return ( |
|
location.hostname.includes("claude.ai") || |
|
!!document.querySelector('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]') || |
|
!!document.querySelector('button[aria-label*="더 많은 옵션"]') |
|
); |
|
}, |
|
|
|
getRoot() { |
|
const firstChatLink = document.querySelector( |
|
'a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]' |
|
); |
|
|
|
return ( |
|
firstChatLink?.closest("ul") || |
|
firstChatLink?.closest("nav") || |
|
firstChatLink?.closest("aside") || |
|
document.querySelector("nav") || |
|
document.querySelector("aside") || |
|
document.body |
|
); |
|
}, |
|
|
|
getScrollContainer() { |
|
return findBestScrollContainer(this.getRoot()); |
|
}, |
|
|
|
forceShowActionButtons() { |
|
if (!document.getElementById("__ai_cleaner_claude_css")) { |
|
const style = document.createElement("style"); |
|
style.id = "__ai_cleaner_claude_css"; |
|
style.textContent = ` |
|
a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"] ~ div button[aria-haspopup="menu"], |
|
a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"] + div button[aria-haspopup="menu"], |
|
button[aria-label*="더 많은 옵션"], |
|
button[aria-label*="more options" i], |
|
button[aria-label*="options" i] { |
|
display: inline-flex !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
} |
|
|
|
div:has(> button[aria-label*="더 많은 옵션"]), |
|
div:has(> button[aria-label*="more options" i]), |
|
div:has(> button[aria-label*="options" i]) { |
|
display: flex !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
|
|
document |
|
.querySelectorAll( |
|
'button[aria-label*="더 많은 옵션"], button[aria-label*="more options" i], button[aria-label*="options" i]' |
|
) |
|
.forEach((button) => { |
|
const parent = button.parentElement; |
|
|
|
button.style.setProperty("display", "inline-flex", "important"); |
|
button.style.setProperty("visibility", "visible", "important"); |
|
button.style.setProperty("opacity", "1", "important"); |
|
button.style.setProperty("pointer-events", "auto", "important"); |
|
|
|
if (parent) { |
|
parent.style.setProperty("display", "flex", "important"); |
|
parent.style.setProperty("visibility", "visible", "important"); |
|
parent.style.setProperty("opacity", "1", "important"); |
|
parent.style.setProperty("pointer-events", "auto", "important"); |
|
} |
|
}); |
|
}, |
|
|
|
getRows() { |
|
this.forceShowActionButtons(); |
|
|
|
const root = this.getRoot(); |
|
|
|
return [...root.querySelectorAll('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]')] |
|
.map((link) => link.closest("li") || link.closest("div.relative.group") || link.parentElement) |
|
.filter(Boolean) |
|
.filter((row, index, arr) => arr.indexOf(row) === index) |
|
.filter(isUsableElement) |
|
.sort( |
|
(a, b) => |
|
a.getBoundingClientRect().top - b.getBoundingClientRect().top |
|
); |
|
}, |
|
|
|
getTitle(row) { |
|
const link = row.querySelector('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]'); |
|
const srOnlyTitle = row.querySelector("[data-find-omitted]"); |
|
const visibleTitle = row.querySelector('span[aria-hidden="true"], span.truncate, .truncate'); |
|
|
|
return ( |
|
getText(srOnlyTitle) || |
|
getText(visibleTitle) || |
|
getText(link) || |
|
link?.getAttribute("href") || |
|
"(제목 없음)" |
|
); |
|
}, |
|
|
|
getKey(row) { |
|
const link = row.querySelector('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]'); |
|
return link?.getAttribute("href") || this.getTitle(row); |
|
}, |
|
|
|
async getMoreButton(row) { |
|
this.forceShowActionButtons(); |
|
|
|
row.scrollIntoView({ block: "center" }); |
|
dispatchHover(row); |
|
|
|
await sleep(350); |
|
|
|
return ( |
|
row.querySelector('button[aria-label*="더 많은 옵션"]') || |
|
row.querySelector('button[aria-label*="more options" i]') || |
|
row.querySelector('button[aria-label*="options" i]') || |
|
row.querySelector('button[aria-haspopup="menu"]') || |
|
null |
|
); |
|
}, |
|
}; |
|
|
|
const MetaCleaner = { |
|
name: "Meta AI", |
|
supportsLazyLoad: true, |
|
|
|
detect() { |
|
return ( |
|
location.hostname.includes("meta.ai") || |
|
!!document.querySelector('[data-testid="conversation-item"] a[href^="/prompt/"]') || |
|
!!document.querySelector('[data-sidebar="menu-action"][aria-label*="옵션"]') |
|
); |
|
}, |
|
|
|
getRoot() { |
|
const firstConversation = document.querySelector('[data-testid="conversation-item"]'); |
|
|
|
return ( |
|
firstConversation?.closest('[data-sidebar="menu"]') || |
|
firstConversation?.closest("ul") || |
|
firstConversation?.closest("nav") || |
|
firstConversation?.closest("aside") || |
|
document.querySelector('[data-sidebar="sidebar"]') || |
|
document.querySelector("nav") || |
|
document.querySelector("aside") || |
|
document.body |
|
); |
|
}, |
|
|
|
getScrollContainer() { |
|
return findBestScrollContainer(this.getRoot()); |
|
}, |
|
|
|
forceShowActionButtons() { |
|
if (!document.getElementById("__ai_cleaner_meta_css")) { |
|
const style = document.createElement("style"); |
|
style.id = "__ai_cleaner_meta_css"; |
|
style.textContent = ` |
|
[data-testid="conversation-item"] [data-sidebar="menu-action"], |
|
[data-testid="conversation-item"] [data-slot="dropdown-menu-trigger"], |
|
[data-testid="conversation-item"] button[aria-label*="옵션"], |
|
[data-testid="conversation-item"] button[aria-label*="options" i] { |
|
display: inline-flex !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
} |
|
|
|
[data-testid="conversation-item"] .contents, |
|
[data-testid="conversation-item"] div:has(> [data-sidebar="menu-action"]) { |
|
display: contents !important; |
|
visibility: visible !important; |
|
opacity: 1 !important; |
|
pointer-events: auto !important; |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
|
|
document |
|
.querySelectorAll( |
|
'[data-testid="conversation-item"] [data-sidebar="menu-action"], [data-testid="conversation-item"] [data-slot="dropdown-menu-trigger"], [data-testid="conversation-item"] button[aria-label*="옵션"], [data-testid="conversation-item"] button[aria-label*="options" i]' |
|
) |
|
.forEach((button) => { |
|
button.style.setProperty("display", "inline-flex", "important"); |
|
button.style.setProperty("visibility", "visible", "important"); |
|
button.style.setProperty("opacity", "1", "important"); |
|
button.style.setProperty("pointer-events", "auto", "important"); |
|
}); |
|
}, |
|
|
|
getRows() { |
|
this.forceShowActionButtons(); |
|
|
|
const root = this.getRoot(); |
|
|
|
return [...root.querySelectorAll('[data-testid="conversation-item"]')] |
|
.filter(isUsableElement) |
|
.filter((row) => row.querySelector('a[href^="/prompt/"]')) |
|
.sort( |
|
(a, b) => |
|
a.getBoundingClientRect().top - b.getBoundingClientRect().top |
|
); |
|
}, |
|
|
|
getTitle(row) { |
|
const link = row.querySelector('a[href^="/prompt/"]'); |
|
const titleElement = |
|
row.querySelector('[data-sidebar="menu-button"] .truncate') || |
|
row.querySelector('a[href^="/prompt/"] span.truncate') || |
|
row.querySelector(".truncate"); |
|
|
|
return ( |
|
getText(titleElement) || |
|
getText(link) || |
|
link?.getAttribute("href") || |
|
"(제목 없음)" |
|
); |
|
}, |
|
|
|
getKey(row) { |
|
const link = row.querySelector('a[href^="/prompt/"]'); |
|
return link?.getAttribute("href") || this.getTitle(row); |
|
}, |
|
|
|
async getMoreButton(row) { |
|
this.forceShowActionButtons(); |
|
|
|
row.scrollIntoView({ block: "center" }); |
|
dispatchHover(row); |
|
|
|
await sleep(350); |
|
|
|
return ( |
|
row.querySelector('[data-sidebar="menu-action"]') || |
|
row.querySelector('[data-slot="dropdown-menu-trigger"]') || |
|
row.querySelector('button[aria-label*="옵션"]') || |
|
row.querySelector('button[aria-label*="options" i]') || |
|
row.querySelector('button[aria-haspopup="menu"]') || |
|
null |
|
); |
|
}, |
|
}; |
|
|
|
const cleaners = [ |
|
GeminiCleaner, |
|
CopilotCleaner, |
|
ClaudeCleaner, |
|
MetaCleaner, |
|
]; |
|
|
|
const activeCleaner = cleaners.find((cleaner) => cleaner.detect()); |
|
|
|
if (!activeCleaner) { |
|
alert( |
|
"지원되는 AI 채팅 서비스 페이지를 감지하지 못했습니다.\n\n" + |
|
`현재 도메인: ${location.hostname}\n\n` + |
|
"지원: Google Gemini, Microsoft Copilot, Claude, Meta AI" |
|
); |
|
return; |
|
} |
|
|
|
const askDeleteLimit = () => { |
|
const input = prompt( |
|
`[${activeCleaner.name}] 삭제할 최근 대화 개수를 입력하세요. 최대 ${CONFIG.maxLimit}개까지 실행됩니다.`, |
|
String(CONFIG.defaultLimit) |
|
); |
|
|
|
const parsed = parseInt(input || "0", 10); |
|
|
|
if (!Number.isFinite(parsed) || parsed <= 0) { |
|
return 0; |
|
} |
|
|
|
return Math.min(parsed, CONFIG.maxLimit); |
|
}; |
|
|
|
const limit = askDeleteLimit(); |
|
|
|
if (!limit) { |
|
console.log("[AI Chat Cleaner] 실행이 취소되었습니다."); |
|
return; |
|
} |
|
|
|
if (CONFIG.requireFinalConfirmation) { |
|
const confirmed = confirm( |
|
`[${activeCleaner.name}] 최근 대화 ${limit}개를 순차적으로 삭제합니다.\n\n` + |
|
"이 작업은 되돌리기 어려울 수 있습니다.\n" + |
|
"계속 진행하시겠습니까?" |
|
); |
|
|
|
if (!confirmed) { |
|
console.log("[AI Chat Cleaner] 사용자가 실행을 취소했습니다."); |
|
return; |
|
} |
|
} |
|
|
|
activeCleaner.forceShowActionButtons(); |
|
|
|
console.log(`[AI Chat Cleaner] 감지된 서비스: ${activeCleaner.name}`); |
|
|
|
if (activeCleaner.supportsLazyLoad) { |
|
await preloadRowsForCleaner(activeCleaner, limit); |
|
} |
|
|
|
let deletedCount = 0; |
|
const deletedKeys = new Set(); |
|
|
|
for (let index = 0; index < limit; index++) { |
|
if (window.__aiChatHistoryCleanerStop) { |
|
console.warn("[AI Chat Cleaner] 중지 요청으로 작업을 멈췄습니다."); |
|
break; |
|
} |
|
|
|
activeCleaner.forceShowActionButtons(); |
|
|
|
await loadMoreIfNeeded(activeCleaner, limit - deletedCount); |
|
|
|
let rowsBefore = activeCleaner |
|
.getRows() |
|
.filter((row) => !deletedKeys.has(activeCleaner.getKey(row))); |
|
|
|
if (rowsBefore.length === 0 && activeCleaner.supportsLazyLoad) { |
|
console.warn( |
|
`[AI Chat Cleaner] 로드된 대화가 없어 ${activeCleaner.name} 추가 로딩을 한 번 더 시도합니다.` |
|
); |
|
|
|
await preloadRowsForCleaner(activeCleaner, limit - deletedCount); |
|
|
|
rowsBefore = activeCleaner |
|
.getRows() |
|
.filter((row) => !deletedKeys.has(activeCleaner.getKey(row))); |
|
} |
|
|
|
const beforeCount = rowsBefore.length; |
|
|
|
if (rowsBefore.length === 0) { |
|
console.warn("[AI Chat Cleaner] 삭제 가능한 대화 행을 찾지 못했습니다."); |
|
break; |
|
} |
|
|
|
const targetRow = rowsBefore[0]; |
|
const targetTitle = activeCleaner.getTitle(targetRow); |
|
const targetKey = activeCleaner.getKey(targetRow); |
|
|
|
console.log( |
|
`[AI Chat Cleaner] ${deletedCount + 1}/${limit} 삭제 대상: ${targetTitle}` |
|
); |
|
|
|
const moreButton = await activeCleaner.getMoreButton(targetRow); |
|
|
|
if (!moreButton) { |
|
console.warn( |
|
`[AI Chat Cleaner] 옵션 버튼을 찾지 못했습니다: ${targetTitle}` |
|
); |
|
break; |
|
} |
|
|
|
await clickElement(moreButton); |
|
await sleep(CONFIG.delayAfterMenuOpen); |
|
|
|
const deleteMenuItem = await findDeleteMenuItem(activeCleaner.name); |
|
|
|
if (!deleteMenuItem) { |
|
console.warn( |
|
`[AI Chat Cleaner] 삭제 메뉴를 찾지 못했습니다: ${targetTitle}` |
|
); |
|
|
|
const visibleMenuItems = [ |
|
...document.querySelectorAll( |
|
[ |
|
'[role="menu"] button', |
|
'[role="menu"] [role="menuitem"]', |
|
'[data-cds="Menu"] [role="menuitem"]', |
|
'[data-radix-popper-content-wrapper] button', |
|
'[data-radix-popper-content-wrapper] [role="menuitem"]', |
|
'[data-radix-popper-content-wrapper] [data-slot="dropdown-menu-item"]', |
|
'[data-radix-menu-content] [data-slot="dropdown-menu-item"]', |
|
'[data-slot*="dropdown"] button', |
|
'[data-slot*="dropdown"] [role="menuitem"]', |
|
'[data-testid="delete-chat-trigger"]', |
|
].join(", ") |
|
), |
|
] |
|
.filter(isVisible) |
|
.map((el) => ({ |
|
text: getText(el), |
|
title: el.getAttribute?.("title"), |
|
aria: el.getAttribute?.("aria-label"), |
|
testId: el.getAttribute?.("data-testid"), |
|
slot: el.getAttribute?.("data-slot"), |
|
className: el.getAttribute?.("class"), |
|
})); |
|
|
|
console.table(visibleMenuItems); |
|
break; |
|
} |
|
|
|
await clickElement(deleteMenuItem); |
|
await sleep(CONFIG.delayAfterDeleteClick); |
|
|
|
const confirmButton = await findConfirmDeleteButton(activeCleaner.name); |
|
|
|
if (confirmButton) { |
|
await clickElement(confirmButton); |
|
await sleep(CONFIG.delayAfterConfirmClick); |
|
|
|
deletedCount += 1; |
|
deletedKeys.add(targetKey); |
|
|
|
console.log(`[AI Chat Cleaner] 삭제 완료: ${deletedCount}개`); |
|
continue; |
|
} |
|
|
|
const changed = await waitForRowCountChange( |
|
activeCleaner, |
|
beforeCount, |
|
targetKey, |
|
CONFIG.delayAfterNoConfirmDelete + 3000 |
|
); |
|
|
|
if (changed) { |
|
deletedCount += 1; |
|
deletedKeys.add(targetKey); |
|
|
|
console.log(`[AI Chat Cleaner] 삭제 완료: ${deletedCount}개`); |
|
await sleep(CONFIG.delayAfterNoConfirmDelete); |
|
continue; |
|
} |
|
|
|
console.warn( |
|
`[AI Chat Cleaner] 삭제 확인 버튼을 찾지 못했고, 목록 변화도 확인되지 않았습니다: ${targetTitle}` |
|
); |
|
break; |
|
} |
|
|
|
alert(`[${activeCleaner.name}] 최근 대화 삭제 작업 완료: ${deletedCount}개`); |
|
})(); |