2586 lines
97 KiB
JavaScript
2586 lines
97 KiB
JavaScript
// 内容脚本 - 在小红书页面上添加收藏功能
|
||
class XHSNoteSaver {
|
||
constructor() {
|
||
this.currentNoteData = null;
|
||
this.saveButton = null;
|
||
this.isAutoClicking = false; // 添加停止标志
|
||
this.processingNotes = new Set(); // 跟踪正在处理的笔记
|
||
this.cleanupTimers = new Set(); // 跟踪清理定时器
|
||
this.maxConcurrentNotes = 3; // 限制并发处理的笔记数量
|
||
this.maxRetries = 3; // 最大重试次数
|
||
this.mutationObserver = null; // 跟踪MutationObserver
|
||
this.detectContentDebounceTimer = null; // 防抖定时器
|
||
this.lastDetectTime = 0; // 上次检测时间
|
||
this.detectInterval = 2000; // 检测间隔(毫秒)
|
||
this.memoryCheckInterval = null; // 内存检查定时器
|
||
this.lastMemoryCheck = 0; // 上次内存检查时间
|
||
this.memoryThreshold = 50 * 1024 * 1024; // 内存阈值(50MB)
|
||
|
||
// 评论模式配置
|
||
this.commentMode = 'custom'; // 'custom' / 'text' / 'image' / 'ai'
|
||
this.customComments = []; // 固定评论池
|
||
this.currentCommentIndex = 0; // 当前评论索引
|
||
this.currentImageIndex = 0; // 当前图片索引(图片回复模式)
|
||
this.imageCommentWarned = false; // 图片失败提示只弹一次
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.createSaveButton();
|
||
this.detectNoteContent();
|
||
this.setupMessageListener();
|
||
|
||
// 监听页面变化 - 使用防抖和间隔控制
|
||
this.mutationObserver = new MutationObserver((mutations) => {
|
||
// 防抖处理,避免频繁调用
|
||
if (this.detectContentDebounceTimer) {
|
||
clearTimeout(this.detectContentDebounceTimer);
|
||
}
|
||
|
||
// 检查时间间隔,避免过于频繁的检测
|
||
const now = Date.now();
|
||
if (now - this.lastDetectTime < this.detectInterval) {
|
||
return;
|
||
}
|
||
|
||
this.detectContentDebounceTimer = setTimeout(() => {
|
||
this.lastDetectTime = Date.now();
|
||
this.detectNoteContent();
|
||
}, 500); // 500ms防抖
|
||
|
||
// 添加到清理定时器集合
|
||
this.cleanupTimers.add(this.detectContentDebounceTimer);
|
||
});
|
||
|
||
this.mutationObserver.observe(document.body, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
|
||
// 监听页面卸载,确保清理资源
|
||
window.addEventListener('beforeunload', () => {
|
||
this.cleanup();
|
||
});
|
||
|
||
// 监听页面隐藏,减少资源消耗
|
||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
||
|
||
// 启动内存监控
|
||
this.startMemoryMonitoring();
|
||
|
||
// 启动内存泄漏防护
|
||
this.setupMemoryLeakProtection();
|
||
|
||
// 初始化自定义评论
|
||
this.initializeCustomComments();
|
||
|
||
// 恢复评论模式设置
|
||
this.restoreCommentMode();
|
||
|
||
// 侧边栏修改评论模式时立即生效(无需刷新页面)
|
||
chrome.storage.onChanged.addListener((changes, namespace) => {
|
||
if (namespace !== 'local' || !changes.commentMode) return;
|
||
const mode = changes.commentMode.newValue;
|
||
if (mode && mode !== this.commentMode) {
|
||
this.commentMode = mode;
|
||
console.log('评论模式已同步:', mode);
|
||
this.updateCommentModeButton();
|
||
this.showToast(`评论模式已切换为: ${this.getCommentModeLabel(mode)}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 恢复评论模式设置
|
||
async restoreCommentMode() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['commentMode']);
|
||
if (result.commentMode) {
|
||
this.commentMode = result.commentMode;
|
||
console.log(`评论模式已恢复: ${this.commentMode}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('恢复评论模式失败:', error);
|
||
}
|
||
}
|
||
|
||
// 初始化自定义评论
|
||
async initializeCustomComments() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['customComments']);
|
||
this.customComments = result.customComments || [];
|
||
|
||
// 如果没有自定义评论,添加一些默认评论
|
||
if (this.customComments.length === 0) {
|
||
const defaultComments = [
|
||
'👍 赞!',
|
||
'💯 太棒了!',
|
||
'🔥 很实用!',
|
||
'👏 学习了!',
|
||
'💪 继续加油!',
|
||
'🌟 分享得很棒!',
|
||
'📚 学到了很多!',
|
||
'🎯 很精准!',
|
||
'💡 很有想法!',
|
||
'❤️ 喜欢!'
|
||
];
|
||
|
||
this.customComments = defaultComments;
|
||
await chrome.storage.local.set({ customComments: defaultComments });
|
||
console.log('已初始化默认自定义评论');
|
||
}
|
||
|
||
console.log(`自定义评论已加载,共${this.customComments.length}条`);
|
||
|
||
} catch (error) {
|
||
console.error('初始化自定义评论失败:', error);
|
||
}
|
||
}
|
||
|
||
// 启动内存监控
|
||
startMemoryMonitoring() {
|
||
if (this.memoryCheckInterval) {
|
||
clearInterval(this.memoryCheckInterval);
|
||
}
|
||
|
||
this.memoryCheckInterval = setInterval(() => {
|
||
this.checkMemoryUsage();
|
||
}, 30000); // 每30秒检查一次,减少频率
|
||
|
||
this.cleanupTimers.add(this.memoryCheckInterval);
|
||
}
|
||
|
||
// 检查内存使用情况
|
||
checkMemoryUsage() {
|
||
try {
|
||
// 检查内存使用(如果浏览器支持)
|
||
if (performance.memory) {
|
||
const usedMemory = performance.memory.usedJSHeapSize;
|
||
const totalMemory = performance.memory.totalJSHeapSize;
|
||
|
||
console.log(`内存使用: ${(usedMemory / 1024 / 1024).toFixed(2)}MB / ${(totalMemory / 1024 / 1024).toFixed(2)}MB`);
|
||
|
||
// 如果内存使用超过阈值,执行清理
|
||
if (usedMemory > this.memoryThreshold) {
|
||
console.warn('内存使用超过阈值,执行紧急清理');
|
||
this.emergencyMemoryCleanup();
|
||
}
|
||
}
|
||
|
||
// 检查DOM节点数量
|
||
const domNodeCount = document.querySelectorAll('*').length;
|
||
if (domNodeCount > 15000) {
|
||
console.warn(`DOM节点数量过多: ${domNodeCount},执行DOM清理`);
|
||
this.cleanupDOMNodes();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('内存检查失败:', error);
|
||
}
|
||
}
|
||
|
||
// 紧急内存清理
|
||
emergencyMemoryCleanup() {
|
||
console.log('执行紧急内存清理...');
|
||
|
||
// 停止所有操作
|
||
this.isAutoClicking = false;
|
||
|
||
// 清理所有定时器
|
||
this.cleanupTimers.forEach(timerId => {
|
||
clearTimeout(timerId);
|
||
clearInterval(timerId);
|
||
});
|
||
this.cleanupTimers.clear();
|
||
|
||
// 清理DOM样式和元素
|
||
this.cleanupHighlightedElements();
|
||
this.cleanupDOMNodes();
|
||
|
||
// 清理进度条
|
||
this.hideProgressBar();
|
||
|
||
// 强制垃圾回收
|
||
if (window.gc) {
|
||
try {
|
||
window.gc();
|
||
console.log('紧急垃圾回收已执行');
|
||
} catch (e) {
|
||
console.log('紧急垃圾回收失败:', e);
|
||
}
|
||
}
|
||
|
||
// 显示清理完成提示
|
||
this.showToast('内存清理完成');
|
||
|
||
console.log('紧急内存清理完成');
|
||
}
|
||
|
||
// 清理DOM节点
|
||
cleanupDOMNodes() {
|
||
try {
|
||
// 清理可能累积的临时元素
|
||
const tempElements = document.querySelectorAll('.xhs-temp-element, .xhs-highlight-temp');
|
||
tempElements.forEach(el => el.remove());
|
||
|
||
// 清理可能的内存泄漏
|
||
const elementsWithLargeData = document.querySelectorAll('[data-large-data]');
|
||
elementsWithLargeData.forEach(el => {
|
||
delete el.dataset.largeData;
|
||
});
|
||
|
||
console.log('DOM节点清理完成');
|
||
} catch (error) {
|
||
console.error('DOM节点清理失败:', error);
|
||
}
|
||
}
|
||
|
||
// 清理所有资源
|
||
cleanup() {
|
||
this.isAutoClicking = false;
|
||
this.processingNotes.clear();
|
||
|
||
// 清理防抖定时器
|
||
if (this.detectContentDebounceTimer) {
|
||
clearTimeout(this.detectContentDebounceTimer);
|
||
this.detectContentDebounceTimer = null;
|
||
}
|
||
|
||
// 清理内存监控定时器
|
||
if (this.memoryCheckInterval) {
|
||
clearInterval(this.memoryCheckInterval);
|
||
this.memoryCheckInterval = null;
|
||
}
|
||
|
||
// 断开MutationObserver
|
||
if (this.mutationObserver) {
|
||
this.mutationObserver.disconnect();
|
||
this.mutationObserver = null;
|
||
}
|
||
|
||
// 清理所有定时器
|
||
this.cleanupTimers.forEach(timerId => {
|
||
clearTimeout(timerId);
|
||
clearInterval(timerId);
|
||
});
|
||
this.cleanupTimers.clear();
|
||
|
||
// 清理DOM样式
|
||
this.cleanupHighlightedElements();
|
||
|
||
// 清理进度条
|
||
this.hideProgressBar();
|
||
|
||
// 清理事件监听器
|
||
this.removeEventListeners();
|
||
|
||
console.log('所有资源已清理');
|
||
}
|
||
|
||
// 移除事件监听器
|
||
removeEventListeners() {
|
||
// 移除页面卸载监听器
|
||
window.removeEventListener('beforeunload', this.cleanup);
|
||
|
||
// 移除页面隐藏监听器
|
||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||
}
|
||
|
||
// 处理页面可见性变化
|
||
handleVisibilityChange = () => {
|
||
if (document.hidden && this.isAutoClicking) {
|
||
console.log('页面隐藏,暂停自动点击');
|
||
this.isAutoClicking = false;
|
||
}
|
||
}
|
||
|
||
// 清理高亮元素
|
||
cleanupHighlightedElements() {
|
||
const highlightedElements = document.querySelectorAll('[style*="outline: 3px solid #4caf50"]');
|
||
highlightedElements.forEach(el => {
|
||
el.style.outline = '';
|
||
el.style.outlineOffset = '';
|
||
el.style.position = '';
|
||
});
|
||
|
||
// 清理标签
|
||
const labels = document.querySelectorAll('[style*="background: #4caf50"][style*="z-index: 10000"]');
|
||
labels.forEach(label => label.remove());
|
||
}
|
||
|
||
createSaveButton() {
|
||
if (this.saveButton) {
|
||
this.saveButton.remove();
|
||
}
|
||
|
||
this.saveButton = document.createElement('button');
|
||
this.saveButton.className = 'xhs-save-btn';
|
||
this.saveButton.textContent = '📝 收藏笔记';
|
||
this.saveButton.style.display = 'none';
|
||
|
||
this.saveButton.addEventListener('click', () => {
|
||
this.handleSaveNote();
|
||
});
|
||
|
||
document.body.appendChild(this.saveButton);
|
||
}
|
||
|
||
detectNoteContent() {
|
||
// 检测是否在笔记详情页
|
||
const isNoteDetailPage = window.location.pathname.includes('/explore/');
|
||
|
||
if (isNoteDetailPage) {
|
||
this.extractNoteData();
|
||
this.showSaveButton();
|
||
} else {
|
||
this.hideSaveButton();
|
||
}
|
||
}
|
||
|
||
extractNoteData() {
|
||
try {
|
||
// 提取笔记标题
|
||
const titleElement = document.querySelector('.note-detail-title') ||
|
||
document.querySelector('[class*="title"]') ||
|
||
document.querySelector('h1');
|
||
|
||
// 提取笔记图片
|
||
const imageElement = document.querySelector('.note-detail-img img') ||
|
||
document.querySelector('[class*="carousel"] img') ||
|
||
document.querySelector('.swiper-slide img') ||
|
||
document.querySelector('img[src*="xiaohongshu"]');
|
||
|
||
// 提取作者信息
|
||
const authorElement = document.querySelector('.user-name') ||
|
||
document.querySelector('[class*="author"]') ||
|
||
document.querySelector('[class*="username"]');
|
||
|
||
// 提取笔记内容
|
||
const contentElement = document.querySelector('.note-detail-desc') ||
|
||
document.querySelector('[class*="content"]') ||
|
||
document.querySelector('[class*="description"]');
|
||
|
||
this.currentNoteData = {
|
||
id: this.extractNoteId(),
|
||
title: titleElement ? titleElement.textContent.trim() : '无标题',
|
||
image: imageElement ? imageElement.src : '',
|
||
author: authorElement ? authorElement.textContent.trim() : '未知作者',
|
||
content: contentElement ? contentElement.textContent.trim() : '',
|
||
url: window.location.href,
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
// 检查是否已经收藏
|
||
this.checkIfSaved();
|
||
|
||
} catch (error) {
|
||
console.error('提取笔记数据失败:', error);
|
||
}
|
||
}
|
||
|
||
extractNoteId() {
|
||
const urlMatch = window.location.pathname.match(/\/explore\/([a-f0-9]+)/);
|
||
return urlMatch ? urlMatch[1] : window.location.href;
|
||
}
|
||
|
||
async checkIfSaved() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['savedNotes']);
|
||
const savedNotes = result.savedNotes || [];
|
||
const isSaved = savedNotes.some(note => note.id === this.currentNoteData.id);
|
||
|
||
this.updateSaveButtonState(isSaved);
|
||
} catch (error) {
|
||
console.error('检查收藏状态失败:', error);
|
||
}
|
||
}
|
||
|
||
updateSaveButtonState(isSaved) {
|
||
if (this.saveButton) {
|
||
if (isSaved) {
|
||
this.saveButton.textContent = '✅ 已收藏';
|
||
this.saveButton.classList.add('saved');
|
||
} else {
|
||
this.saveButton.textContent = '📝 收藏笔记';
|
||
this.saveButton.classList.remove('saved');
|
||
}
|
||
}
|
||
}
|
||
|
||
showSaveButton() {
|
||
if (this.saveButton) {
|
||
this.saveButton.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
hideSaveButton() {
|
||
if (this.saveButton) {
|
||
this.saveButton.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async handleSaveNote() {
|
||
if (!this.currentNoteData) {
|
||
this.showToast('无法获取笔记信息');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 检查是否已经收藏
|
||
const result = await chrome.storage.local.get(['savedNotes', 'tags']);
|
||
const savedNotes = result.savedNotes || [];
|
||
const existingIndex = savedNotes.findIndex(note => note.id === this.currentNoteData.id);
|
||
|
||
if (existingIndex >= 0) {
|
||
// 如果已收藏,则取消收藏
|
||
savedNotes.splice(existingIndex, 1);
|
||
await chrome.storage.local.set({ savedNotes });
|
||
this.updateSaveButtonState(false);
|
||
this.showToast('已取消收藏');
|
||
} else {
|
||
// 如果未收藏,显示标签选择弹窗
|
||
this.showTagModal();
|
||
}
|
||
} catch (error) {
|
||
console.error('保存笔记失败:', error);
|
||
this.showToast('保存失败,请重试');
|
||
}
|
||
}
|
||
|
||
async showTagModal() {
|
||
// 获取已有标签
|
||
const result = await chrome.storage.local.get(['tags']);
|
||
const tags = result.tags || [];
|
||
|
||
// 创建弹窗
|
||
const modal = document.createElement('div');
|
||
modal.className = 'xhs-tag-modal';
|
||
|
||
modal.innerHTML = `
|
||
<div class="xhs-tag-modal-content">
|
||
<h3>选择标签</h3>
|
||
<input type="text" class="xhs-tag-input" placeholder="添加新标签...">
|
||
<div class="xhs-tag-list">
|
||
${tags.map(tag => `
|
||
<div class="xhs-tag-item" data-tag="${tag.name}">
|
||
${tag.name} (${tag.count || 0})
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div class="xhs-modal-actions">
|
||
<button class="xhs-modal-btn xhs-secondary-btn" id="cancel-save">取消</button>
|
||
<button class="xhs-modal-btn xhs-primary-btn" id="confirm-save">保存</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 绑定事件
|
||
const tagInput = modal.querySelector('.xhs-tag-input');
|
||
const tagItems = modal.querySelectorAll('.xhs-tag-item');
|
||
const cancelBtn = modal.querySelector('#cancel-save');
|
||
const confirmBtn = modal.querySelector('#confirm-save');
|
||
|
||
let selectedTags = [];
|
||
|
||
// 标签选择
|
||
tagItems.forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const tagName = item.dataset.tag;
|
||
if (item.classList.contains('selected')) {
|
||
item.classList.remove('selected');
|
||
selectedTags = selectedTags.filter(tag => tag !== tagName);
|
||
} else {
|
||
item.classList.add('selected');
|
||
selectedTags.push(tagName);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 新标签输入
|
||
tagInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
const newTag = tagInput.value.trim();
|
||
if (newTag && !selectedTags.includes(newTag)) {
|
||
selectedTags.push(newTag);
|
||
|
||
// 添加到标签列表显示
|
||
const tagList = modal.querySelector('.xhs-tag-list');
|
||
const newTagElement = document.createElement('div');
|
||
newTagElement.className = 'xhs-tag-item selected';
|
||
newTagElement.dataset.tag = newTag;
|
||
newTagElement.textContent = newTag;
|
||
tagList.appendChild(newTagElement);
|
||
|
||
newTagElement.addEventListener('click', () => {
|
||
newTagElement.classList.remove('selected');
|
||
selectedTags = selectedTags.filter(tag => tag !== newTag);
|
||
newTagElement.remove();
|
||
});
|
||
|
||
tagInput.value = '';
|
||
}
|
||
}
|
||
});
|
||
|
||
// 取消按钮
|
||
cancelBtn.addEventListener('click', () => {
|
||
modal.remove();
|
||
});
|
||
|
||
// 确认保存
|
||
confirmBtn.addEventListener('click', async () => {
|
||
await this.saveNoteWithTags(selectedTags);
|
||
modal.remove();
|
||
});
|
||
|
||
// 点击背景关闭
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
async saveNoteWithTags(selectedTags) {
|
||
try {
|
||
// 保存笔记
|
||
const result = await chrome.storage.local.get(['savedNotes', 'tags']);
|
||
const savedNotes = result.savedNotes || [];
|
||
const tags = result.tags || [];
|
||
|
||
// 添加标签到笔记
|
||
this.currentNoteData.tags = selectedTags;
|
||
savedNotes.push(this.currentNoteData);
|
||
|
||
// 更新标签计数
|
||
selectedTags.forEach(tagName => {
|
||
const existingTag = tags.find(tag => tag.name === tagName);
|
||
if (existingTag) {
|
||
existingTag.count = (existingTag.count || 0) + 1;
|
||
} else {
|
||
tags.push({ name: tagName, count: 1 });
|
||
}
|
||
});
|
||
|
||
// 保存到存储
|
||
await chrome.storage.local.set({
|
||
savedNotes,
|
||
tags
|
||
});
|
||
|
||
this.updateSaveButtonState(true);
|
||
this.showToast(`已收藏 ${selectedTags.length > 0 ? '并添加标签' : ''}`);
|
||
|
||
} catch (error) {
|
||
console.error('保存笔记失败:', error);
|
||
this.showToast('保存失败,请重试');
|
||
}
|
||
}
|
||
|
||
showToast(message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'xhs-toast';
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
setupMessageListener() {
|
||
console.log('设置消息监听器...');
|
||
|
||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||
console.log('收到消息:', request, '来自:', sender);
|
||
|
||
if (request.action === 'getCurrentNote') {
|
||
console.log('处理获取当前笔记请求');
|
||
sendResponse({
|
||
noteData: this.currentNoteData,
|
||
isNotePage: this.currentNoteData !== null
|
||
});
|
||
}
|
||
|
||
if (request.action === 'autoClickNotes') {
|
||
console.log('收到自动点击请求,关键词:', request.keyword);
|
||
|
||
// 立即响应,表示消息已收到
|
||
sendResponse({ success: true, message: '开始自动点击' });
|
||
|
||
// 开始自动点击
|
||
this.startAutoClickNotes();
|
||
}
|
||
|
||
if (request.action === 'testElementDetection') {
|
||
console.log('收到测试元素检测请求');
|
||
|
||
// 测试元素检测
|
||
const primarySelector = '#global > div.main-container > div.with-side-bar.main-content > div > div > div.search-layout__main > div.feeds-container > section';
|
||
let primaryElements = [];
|
||
|
||
try {
|
||
primaryElements = document.querySelectorAll(primarySelector);
|
||
} catch (error) {
|
||
console.log('主要选择器测试失败:', error);
|
||
}
|
||
|
||
const elements = this.findNoteElements();
|
||
const visibleElements = this.findVisibleNoteElements();
|
||
|
||
// 分析第一个元素的详细结构
|
||
let elementAnalysis = [];
|
||
if (elements.length > 0) {
|
||
const firstElement = elements[0];
|
||
const links = firstElement.querySelectorAll('a');
|
||
const buttons = firstElement.querySelectorAll('button, [role="button"]');
|
||
const clickableDivs = firstElement.querySelectorAll('div[class*="click"], div[class*="expand"], div[class*="more"]');
|
||
|
||
// 专门检测目标链接
|
||
const targetLink = firstElement.querySelector('div > a.cover.mask.ld');
|
||
|
||
elementAnalysis = {
|
||
tagName: firstElement.tagName,
|
||
className: firstElement.className,
|
||
noteIndex: firstElement.dataset.noteIndex,
|
||
selector: firstElement.dataset.selector,
|
||
targetLink: targetLink ? {
|
||
href: targetLink.href,
|
||
text: targetLink.textContent.trim(),
|
||
className: targetLink.className,
|
||
found: true
|
||
} : {
|
||
found: false,
|
||
message: '未找到目标链接 div > a.cover.mask.ld'
|
||
},
|
||
links: Array.from(links).map(link => ({
|
||
href: link.href,
|
||
text: link.textContent.trim(),
|
||
className: link.className
|
||
})),
|
||
buttons: Array.from(buttons).map(btn => ({
|
||
text: btn.textContent.trim(),
|
||
className: btn.className,
|
||
role: btn.getAttribute('role')
|
||
})),
|
||
clickableDivs: Array.from(clickableDivs).map(div => ({
|
||
text: div.textContent.trim(),
|
||
className: div.className
|
||
}))
|
||
};
|
||
}
|
||
|
||
const result = {
|
||
success: true,
|
||
primarySelector: primarySelector,
|
||
primaryElementsCount: primaryElements.length,
|
||
totalElements: elements.length,
|
||
visibleElements: visibleElements.length,
|
||
elementDetails: elements.slice(0, 5).map(el => ({
|
||
tagName: el.tagName,
|
||
className: el.className,
|
||
hasLink: !!el.querySelector('a[href*="/explore/"], a[href*="/discovery/"], a[href*="/note/"]'),
|
||
hasImage: !!el.querySelector('img'),
|
||
textLength: el.textContent ? el.textContent.trim().length : 0
|
||
})),
|
||
firstElementAnalysis: elementAnalysis
|
||
};
|
||
|
||
console.log('测试结果:', result);
|
||
sendResponse(result);
|
||
}
|
||
|
||
// 返回true表示异步响应
|
||
return true;
|
||
});
|
||
|
||
console.log('消息监听器设置完成');
|
||
}
|
||
|
||
// 自动点击笔记功能
|
||
async startAutoClickNotes() {
|
||
console.log('开始自动点击笔记...');
|
||
|
||
try {
|
||
// 检查内存使用情况
|
||
this.checkMemoryUsage();
|
||
|
||
// 设置自动点击状态
|
||
this.isAutoClicking = true;
|
||
|
||
// 显示进度条
|
||
this.showProgressBar();
|
||
|
||
// 等待页面完全加载
|
||
await this.waitForPageLoad();
|
||
|
||
// 高亮显示检测到的元素(限制数量)
|
||
this.highlightDetectedElements();
|
||
|
||
// 开始自动点击
|
||
await this.autoClickVisibleNotes();
|
||
|
||
} catch (error) {
|
||
console.error('自动点击过程中发生错误:', error);
|
||
this.showToast('自动点击出错,已停止');
|
||
} finally {
|
||
// 隐藏进度条
|
||
this.hideProgressBar();
|
||
|
||
// 重置状态
|
||
this.isAutoClicking = false;
|
||
|
||
// 执行清理
|
||
this.performMemoryCleanup();
|
||
}
|
||
}
|
||
|
||
// 等待页面加载完成
|
||
async waitForPageLoad() {
|
||
return new Promise((resolve, reject) => {
|
||
let attempts = 0;
|
||
const maxAttempts = 20; // 最多等待10秒
|
||
|
||
const checkNotes = () => {
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
reject(new Error('等待页面加载被停止'));
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
const notes = this.findNoteElements();
|
||
if (notes.length > 0) {
|
||
resolve();
|
||
} else if (attempts >= maxAttempts) {
|
||
reject(new Error('等待页面加载超时'));
|
||
} else {
|
||
const timerId = setTimeout(checkNotes, 500);
|
||
this.cleanupTimers.add(timerId);
|
||
}
|
||
};
|
||
checkNotes();
|
||
});
|
||
}
|
||
|
||
// 查找笔记元素
|
||
findNoteElements() {
|
||
console.log('开始查找笔记元素...');
|
||
|
||
// 使用用户提供的精确选择器,查找所有笔记
|
||
const baseSelector = '#global > div.main-container > div.with-side-bar.main-content > div > div > div.search-layout__main > div.feeds-container > section';
|
||
|
||
console.log('使用基础选择器:', baseSelector);
|
||
|
||
// 查找所有section元素
|
||
const allSections = document.querySelectorAll(baseSelector);
|
||
console.log(`找到 ${allSections.length} 个section元素`);
|
||
|
||
if (allSections.length > 0) {
|
||
// 为每个section添加索引信息
|
||
const notesWithIndex = Array.from(allSections).map((section, index) => {
|
||
section.dataset.noteIndex = index + 1;
|
||
section.dataset.selector = `${baseSelector}:nth-child(${index + 1})`;
|
||
return section;
|
||
});
|
||
|
||
console.log('笔记元素已添加索引信息');
|
||
return notesWithIndex;
|
||
}
|
||
|
||
// 如果主要选择器失败,使用备用选择器
|
||
console.log('主要选择器未找到元素,尝试备用选择器...');
|
||
const fallbackSelectors = [
|
||
'.note-item',
|
||
'.search-item',
|
||
'.feed-item',
|
||
'.card-item',
|
||
'.note-card',
|
||
'.search-note-item',
|
||
'.search-result-item',
|
||
'.note-list-item'
|
||
];
|
||
|
||
for (const selector of fallbackSelectors) {
|
||
try {
|
||
const elements = document.querySelectorAll(selector);
|
||
if (elements.length > 0) {
|
||
console.log(`备用选择器 "${selector}" 找到 ${elements.length} 个元素`);
|
||
return Array.from(elements);
|
||
}
|
||
} catch (error) {
|
||
console.log(`备用选择器 "${selector}" 无效:`, error);
|
||
}
|
||
}
|
||
|
||
console.log('未找到任何笔记元素');
|
||
return [];
|
||
}
|
||
|
||
// 自动点击可见的笔记
|
||
async autoClickVisibleNotes() {
|
||
let totalClicked = 0;
|
||
let hasMoreNotes = true;
|
||
let totalFound = 0;
|
||
let consecutiveFailures = 0; // 连续失败计数
|
||
const maxConsecutiveFailures = 5; // 最大连续失败次数
|
||
|
||
try {
|
||
while (hasMoreNotes && this.isAutoClicking) {
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('自动点击被停止');
|
||
break;
|
||
}
|
||
|
||
// 检查连续失败次数
|
||
if (consecutiveFailures >= maxConsecutiveFailures) {
|
||
console.log(`连续失败${maxConsecutiveFailures}次,停止自动点击`);
|
||
this.showToast('连续失败次数过多,已停止自动点击');
|
||
break;
|
||
}
|
||
|
||
// 查找当前可见的笔记
|
||
const visibleNotes = this.findVisibleNoteElements();
|
||
|
||
if (visibleNotes.length === 0) {
|
||
console.log('没有找到更多笔记');
|
||
break;
|
||
}
|
||
|
||
totalFound += visibleNotes.length;
|
||
console.log(`找到 ${visibleNotes.length} 个可见笔记`);
|
||
|
||
// 更新进度条
|
||
this.updateProgress(totalClicked, totalFound);
|
||
|
||
// 限制并发处理的笔记数量
|
||
const notesToProcess = visibleNotes.slice(0, this.maxConcurrentNotes);
|
||
|
||
// 依次点击每个笔记
|
||
for (let i = 0; i < notesToProcess.length && this.isAutoClicking; i++) {
|
||
const note = notesToProcess[i];
|
||
|
||
try {
|
||
// 检查是否已经点击过
|
||
if (note.dataset.clicked === 'true') {
|
||
continue;
|
||
}
|
||
|
||
// 检查是否正在处理
|
||
if (this.processingNotes.has(note)) {
|
||
continue;
|
||
}
|
||
|
||
// 标记为正在处理
|
||
this.processingNotes.add(note);
|
||
|
||
// 标记为已点击
|
||
note.dataset.clicked = 'true';
|
||
|
||
// 点击笔记(展开内容)
|
||
await this.clickNote(note);
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('自动点击被停止');
|
||
break;
|
||
}
|
||
|
||
totalClicked++;
|
||
consecutiveFailures = 0; // 重置失败计数
|
||
|
||
// 更新进度条
|
||
this.updateProgress(totalClicked, totalFound);
|
||
|
||
console.log(`已点击第 ${totalClicked} 个笔记`);
|
||
|
||
// 评论间隔:在设定范围内随机等待(如1~2分钟),模拟真人节奏
|
||
await this.randomCommentDelay();
|
||
|
||
} catch (error) {
|
||
console.error('点击笔记失败:', error);
|
||
consecutiveFailures++;
|
||
} finally {
|
||
// 清理处理状态
|
||
this.processingNotes.delete(note);
|
||
}
|
||
}
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('自动点击被停止');
|
||
break;
|
||
}
|
||
|
||
// 滚动页面加载更多笔记
|
||
try {
|
||
hasMoreNotes = await this.scrollToLoadMore();
|
||
|
||
if (hasMoreNotes) {
|
||
// 等待新内容加载
|
||
await this.sleep(2000);
|
||
}
|
||
} catch (error) {
|
||
console.error('滚动加载失败:', error);
|
||
hasMoreNotes = false;
|
||
}
|
||
|
||
// 添加内存清理
|
||
if (totalClicked % 10 === 0) {
|
||
this.performMemoryCleanup();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('自动点击过程中发生错误:', error);
|
||
} finally {
|
||
// 清理所有资源
|
||
this.cleanup();
|
||
}
|
||
|
||
console.log(`自动点击完成,总共点击了 ${totalClicked} 个笔记`);
|
||
this.showToast(`自动点击完成,共点击 ${totalClicked} 个笔记`);
|
||
}
|
||
|
||
// 查找可见的笔记元素
|
||
findVisibleNoteElements() {
|
||
const notes = this.findNoteElements();
|
||
const visibleNotes = [];
|
||
|
||
for (const note of notes) {
|
||
if (this.isElementVisible(note) && note.dataset.clicked !== 'true') {
|
||
visibleNotes.push(note);
|
||
}
|
||
}
|
||
|
||
return visibleNotes;
|
||
}
|
||
|
||
// 检查元素是否可见
|
||
isElementVisible(element) {
|
||
const rect = element.getBoundingClientRect();
|
||
const windowHeight = window.innerHeight;
|
||
|
||
// 元素在视口内且有一定高度
|
||
return rect.top < windowHeight && rect.bottom > 0 && rect.height > 50;
|
||
}
|
||
|
||
// 点击笔记
|
||
async clickNote(noteElement) {
|
||
console.log('尝试点击笔记元素:', noteElement);
|
||
console.log('元素标签:', noteElement.tagName, '类名:', noteElement.className);
|
||
|
||
// 获取用户选择的点击模式
|
||
const shouldExpand = await this.getClickMode();
|
||
console.log('点击模式 - 展开内容:', shouldExpand);
|
||
|
||
if (shouldExpand) {
|
||
// 展开内容模式
|
||
await this.expandNoteContent(noteElement);
|
||
} else {
|
||
// 简单点击模式
|
||
await this.simpleClickNote(noteElement);
|
||
}
|
||
}
|
||
|
||
// 获取用户选择的点击模式
|
||
async getClickMode() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['expandContent']);
|
||
return result.expandContent !== false; // 默认展开内容
|
||
} catch (error) {
|
||
console.log('获取点击模式失败,使用默认模式:', error);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 展开笔记内容
|
||
async expandNoteContent(noteElement) {
|
||
console.log(`处理笔记 ${noteElement.dataset.noteIndex || '未知'}:`, noteElement.dataset.selector || '未知选择器');
|
||
|
||
// 专门查找并点击 div > a.cover.mask.ld 链接
|
||
const targetLink = noteElement.querySelector('div > a.cover.mask.ld');
|
||
|
||
if (targetLink) {
|
||
console.log('找到目标链接:', targetLink.href);
|
||
console.log('链接文本:', targetLink.textContent.trim());
|
||
console.log('链接类名:', targetLink.className);
|
||
|
||
try {
|
||
// 滚动到链接位置,确保可见
|
||
targetLink.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
await this.sleep(500);
|
||
|
||
// 点击链接
|
||
targetLink.click();
|
||
console.log('成功点击目标链接');
|
||
|
||
// 等待内容加载并执行评论交互流程
|
||
await this.executeCommentInteraction();
|
||
|
||
return;
|
||
} catch (error) {
|
||
console.log('点击目标链接失败:', error);
|
||
}
|
||
} else {
|
||
console.log('未找到目标链接 div > a.cover.mask.ld');
|
||
}
|
||
|
||
// 如果没找到目标链接,尝试其他方法
|
||
console.log('尝试备用点击方法...');
|
||
|
||
// 查找笔记中的其他可点击元素,但排除链接,只选择能展开内容的元素
|
||
const clickableElements = noteElement.querySelectorAll('[role="button"], [tabindex], [onclick], [class*="click"], [class*="expand"], [class*="open"], [class*="toggle"], [class*="more"], [class*="view"]');
|
||
console.log(`找到 ${clickableElements.length} 个其他可点击元素`);
|
||
|
||
// 优先选择看起来像能展开内容的元素
|
||
let bestTarget = null;
|
||
|
||
for (const clickable of clickableElements) {
|
||
const className = clickable.className.toLowerCase();
|
||
const text = clickable.textContent.toLowerCase();
|
||
|
||
// 检查是否看起来像展开按钮
|
||
if (className.includes('expand') || className.includes('open') ||
|
||
className.includes('click') || className.includes('toggle') ||
|
||
className.includes('more') || className.includes('view') ||
|
||
text.includes('展开') || text.includes('更多') || text.includes('查看') ||
|
||
text.includes('显示') || text.includes('详情')) {
|
||
bestTarget = clickable;
|
||
console.log('找到展开按钮:', clickable.tagName, clickable.className, clickable.textContent);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果没有找到明确的展开按钮,尝试查找其他可点击元素(但绝对不是链接)
|
||
if (!bestTarget) {
|
||
// 查找按钮、div等非链接元素
|
||
const buttonElements = noteElement.querySelectorAll('button, [role="button"], div[class*="click"], div[class*="expand"], div[class*="more"]');
|
||
console.log(`找到 ${buttonElements.length} 个按钮元素`);
|
||
|
||
if (buttonElements.length > 0) {
|
||
bestTarget = buttonElements[0];
|
||
console.log('选择按钮元素:', bestTarget.tagName, bestTarget.className);
|
||
}
|
||
}
|
||
|
||
// 尝试点击选中的目标
|
||
if (bestTarget) {
|
||
try {
|
||
console.log('尝试点击备用目标元素:', bestTarget.tagName, bestTarget.className);
|
||
|
||
// 滚动到元素位置,确保可见
|
||
bestTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
await this.sleep(500);
|
||
|
||
// 模拟点击
|
||
bestTarget.click();
|
||
console.log('成功点击备用目标元素');
|
||
|
||
// 等待内容展开并执行评论交互流程
|
||
await this.executeCommentInteraction();
|
||
|
||
return;
|
||
} catch (error) {
|
||
console.log('点击备用目标元素失败:', error);
|
||
}
|
||
}
|
||
|
||
// 如果所有方法都失败了,尝试点击整个笔记元素(但阻止默认行为)
|
||
try {
|
||
console.log('尝试点击整个笔记元素(阻止默认行为)');
|
||
|
||
// 滚动到笔记位置
|
||
noteElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
await this.sleep(500);
|
||
|
||
// 模拟鼠标点击,但阻止默认行为
|
||
this.simulateClickWithPreventDefault(noteElement);
|
||
console.log('成功点击整个笔记元素');
|
||
|
||
// 等待内容展开并执行评论交互流程
|
||
await this.executeCommentInteraction();
|
||
|
||
} catch (error) {
|
||
console.log('点击整个笔记元素失败:', error);
|
||
}
|
||
}
|
||
|
||
// 简单点击笔记
|
||
async simpleClickNote(noteElement) {
|
||
try {
|
||
console.log(`执行简单点击模式 - 笔记 ${noteElement.dataset.noteIndex || '未知'}`);
|
||
|
||
// 优先查找并点击目标链接
|
||
const targetLink = noteElement.querySelector('div > a.cover.mask.ld');
|
||
|
||
if (targetLink) {
|
||
console.log('找到目标链接,执行简单点击');
|
||
|
||
// 滚动到链接位置
|
||
targetLink.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
await this.sleep(300);
|
||
|
||
// 点击链接
|
||
targetLink.click();
|
||
console.log('成功点击目标链接(简单模式)');
|
||
|
||
// 等待内容加载并执行评论交互流程
|
||
await this.executeCommentInteraction();
|
||
|
||
return;
|
||
}
|
||
|
||
// 如果没有找到目标链接,执行原来的简单点击逻辑
|
||
console.log('未找到目标链接,执行备用简单点击');
|
||
|
||
// 滚动到笔记位置
|
||
noteElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
await this.sleep(300);
|
||
|
||
// 直接点击整个笔记元素,但阻止默认行为
|
||
this.simulateClickWithPreventDefault(noteElement);
|
||
console.log('成功执行备用简单点击');
|
||
|
||
// 短暂等待
|
||
await this.sleep(500);
|
||
|
||
} catch (error) {
|
||
console.log('简单点击失败:', error);
|
||
}
|
||
}
|
||
|
||
// 模拟鼠标点击
|
||
simulateClick(element) {
|
||
const rect = element.getBoundingClientRect();
|
||
const centerX = rect.left + rect.width / 2;
|
||
const centerY = rect.top + rect.height / 2;
|
||
|
||
// 创建鼠标事件
|
||
const clickEvent = new MouseEvent('click', {
|
||
view: window,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
clientX: centerX,
|
||
clientY: centerY
|
||
});
|
||
|
||
element.dispatchEvent(clickEvent);
|
||
}
|
||
|
||
// 模拟鼠标点击并阻止默认行为
|
||
simulateClickWithPreventDefault(element) {
|
||
const rect = element.getBoundingClientRect();
|
||
const centerX = rect.left + rect.width / 2;
|
||
const centerY = rect.top + rect.height / 2;
|
||
|
||
const clickEvent = new MouseEvent('click', {
|
||
view: window,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
clientX: centerX,
|
||
clientY: centerY
|
||
});
|
||
|
||
element.dispatchEvent(clickEvent);
|
||
clickEvent.preventDefault(); // 阻止默认行为
|
||
}
|
||
|
||
// 显示进度条
|
||
showProgressBar() {
|
||
// 移除已存在的进度条
|
||
this.hideProgressBar();
|
||
|
||
const progressBar = document.createElement('div');
|
||
progressBar.className = 'xhs-progress-bar';
|
||
progressBar.innerHTML = `
|
||
<div class="xhs-progress-container">
|
||
<div class="xhs-progress-text">🤖 自动点击进行中...</div>
|
||
<div class="xhs-progress-bar-inner">
|
||
<div class="xhs-progress-fill"></div>
|
||
</div>
|
||
<div class="xhs-progress-stats">
|
||
<span id="xhs-clicked-count">已点击: 0</span>
|
||
<span id="xhs-total-count">总计: 0</span>
|
||
<span id="xhs-memory-usage">内存: --</span>
|
||
</div>
|
||
<div class="xhs-progress-tip">
|
||
💡 长按评论模式按钮可管理自定义评论
|
||
</div>
|
||
<div class="xhs-progress-actions">
|
||
<button class="xhs-stop-btn" id="xhs-stop-auto-click">⏹️ 停止</button>
|
||
<button class="xhs-cleanup-btn" id="xhs-cleanup-memory">🧹 清理内存</button>
|
||
<button class="xhs-comment-mode-btn" id="xhs-comment-mode">🤖 AI评论</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(progressBar);
|
||
|
||
// 绑定停止按钮事件
|
||
const stopBtn = progressBar.querySelector('#xhs-stop-auto-click');
|
||
stopBtn.addEventListener('click', () => {
|
||
this.stopAutoClick();
|
||
});
|
||
|
||
// 绑定清理内存按钮事件
|
||
const cleanupBtn = progressBar.querySelector('#xhs-cleanup-memory');
|
||
cleanupBtn.addEventListener('click', () => {
|
||
this.emergencyMemoryCleanup();
|
||
});
|
||
|
||
// 绑定评论模式切换按钮事件
|
||
const commentModeBtn = progressBar.querySelector('#xhs-comment-mode');
|
||
commentModeBtn.addEventListener('click', () => {
|
||
this.toggleCommentMode();
|
||
});
|
||
|
||
// 长按显示自定义评论管理弹窗
|
||
let longPressTimer = null;
|
||
commentModeBtn.addEventListener('mousedown', () => {
|
||
longPressTimer = setTimeout(() => {
|
||
if (this.commentMode === 'custom') {
|
||
this.showCustomCommentModal();
|
||
}
|
||
}, 800); // 800ms长按
|
||
});
|
||
|
||
commentModeBtn.addEventListener('mouseup', () => {
|
||
if (longPressTimer) {
|
||
clearTimeout(longPressTimer);
|
||
longPressTimer = null;
|
||
}
|
||
});
|
||
|
||
commentModeBtn.addEventListener('mouseleave', () => {
|
||
if (longPressTimer) {
|
||
clearTimeout(longPressTimer);
|
||
longPressTimer = null;
|
||
}
|
||
});
|
||
|
||
// 更新评论模式按钮显示
|
||
this.updateCommentModeButton();
|
||
|
||
// 添加动画效果
|
||
setTimeout(() => {
|
||
progressBar.classList.add('show');
|
||
}, 100);
|
||
|
||
// 启动内存使用更新
|
||
this.startMemoryUsageUpdate();
|
||
}
|
||
|
||
// 启动内存使用更新
|
||
startMemoryUsageUpdate() {
|
||
const updateMemory = () => {
|
||
if (!this.isAutoClicking) return;
|
||
|
||
const memoryElement = document.getElementById('xhs-memory-usage');
|
||
if (memoryElement) {
|
||
const stats = this.getMemoryStats();
|
||
if (stats) {
|
||
memoryElement.textContent = `内存: ${stats.usedMB}MB`;
|
||
|
||
// 根据内存使用情况改变颜色
|
||
if (stats.used > this.memoryThreshold) {
|
||
memoryElement.style.color = '#ff4444'; // 红色警告
|
||
} else if (stats.used > this.memoryThreshold * 0.7) {
|
||
memoryElement.style.color = '#ffaa00'; // 黄色警告
|
||
} else {
|
||
memoryElement.style.color = '#44ff44'; // 绿色正常
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 立即更新一次
|
||
updateMemory();
|
||
|
||
// 每5秒更新一次
|
||
const updateTimer = setInterval(updateMemory, 5000);
|
||
this.cleanupTimers.add(updateTimer);
|
||
}
|
||
|
||
// 高亮显示检测到的元素
|
||
highlightDetectedElements() {
|
||
// 先清理之前的高亮
|
||
this.cleanupHighlightedElements();
|
||
|
||
const elements = this.findNoteElements();
|
||
console.log(`高亮显示 ${elements.length} 个检测到的元素`);
|
||
|
||
// 限制高亮元素数量,避免内存问题
|
||
const maxHighlightElements = Math.min(elements.length, 20);
|
||
const elementsToHighlight = elements.slice(0, maxHighlightElements);
|
||
|
||
elementsToHighlight.forEach((el, index) => {
|
||
// 添加高亮样式
|
||
el.style.outline = '3px solid #4caf50';
|
||
el.style.outlineOffset = '2px';
|
||
el.style.position = 'relative';
|
||
|
||
// 添加序号标签
|
||
const label = document.createElement('div');
|
||
label.style.cssText = `
|
||
position: absolute;
|
||
top: -10px;
|
||
left: -10px;
|
||
background: #4caf50;
|
||
color: white;
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
z-index: 10000;
|
||
pointer-events: none;
|
||
`;
|
||
label.textContent = index + 1;
|
||
el.appendChild(label);
|
||
|
||
// 设置清理定时器
|
||
const cleanupTimer = setTimeout(() => {
|
||
if (el.style.outline) {
|
||
el.style.outline = '';
|
||
el.style.outlineOffset = '';
|
||
el.style.position = '';
|
||
if (label.parentNode) {
|
||
label.remove();
|
||
}
|
||
}
|
||
// 从清理定时器集合中移除
|
||
this.cleanupTimers.delete(cleanupTimer);
|
||
}, 3000);
|
||
|
||
// 添加到清理定时器集合
|
||
this.cleanupTimers.add(cleanupTimer);
|
||
});
|
||
|
||
// 如果元素数量超过限制,显示提示
|
||
if (elements.length > maxHighlightElements) {
|
||
console.log(`高亮显示限制为${maxHighlightElements}个元素,避免内存问题`);
|
||
}
|
||
}
|
||
|
||
// 停止自动点击
|
||
stopAutoClick() {
|
||
console.log('用户停止自动点击');
|
||
this.isAutoClicking = false;
|
||
|
||
// 清理所有资源
|
||
this.cleanup();
|
||
|
||
// 显示停止提示
|
||
this.showToast('已停止自动点击');
|
||
|
||
console.log('自动点击已完全停止');
|
||
}
|
||
|
||
// 强制停止所有操作
|
||
forceStopAllOperations() {
|
||
console.log('强制停止所有操作...');
|
||
|
||
// 清理所有资源
|
||
this.cleanup();
|
||
|
||
console.log('所有操作已被强制停止');
|
||
}
|
||
|
||
// 获取内存使用统计
|
||
getMemoryStats() {
|
||
try {
|
||
if (performance.memory) {
|
||
return {
|
||
used: performance.memory.usedJSHeapSize,
|
||
total: performance.memory.totalJSHeapSize,
|
||
limit: performance.memory.jsHeapSizeLimit,
|
||
usedMB: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2),
|
||
totalMB: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2),
|
||
limitMB: (performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)
|
||
};
|
||
}
|
||
return null;
|
||
} catch (error) {
|
||
console.error('获取内存统计失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 隐藏进度条
|
||
hideProgressBar() {
|
||
const existingBar = document.querySelector('.xhs-progress-bar');
|
||
if (existingBar) {
|
||
existingBar.remove();
|
||
}
|
||
}
|
||
|
||
// 更新进度条
|
||
updateProgress(clicked, total) {
|
||
const clickedElement = document.getElementById('xhs-clicked-count');
|
||
const totalElement = document.getElementById('xhs-total-count');
|
||
const fillElement = document.querySelector('.xhs-progress-fill');
|
||
|
||
if (clickedElement) clickedElement.textContent = `已点击: ${clicked}`;
|
||
if (totalElement) totalElement.textContent = `总计: ${total}`;
|
||
|
||
if (fillElement && total > 0) {
|
||
const percentage = Math.min((clicked / total) * 100, 100);
|
||
fillElement.style.width = `${percentage}%`;
|
||
}
|
||
}
|
||
|
||
// 滚动加载更多内容
|
||
async scrollToLoadMore() {
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('滚动加载被停止');
|
||
return false;
|
||
}
|
||
|
||
const scrollHeight = document.documentElement.scrollHeight;
|
||
const scrollTop = window.pageYOffset;
|
||
const clientHeight = document.documentElement.clientHeight;
|
||
|
||
// 如果已经滚动到底部,尝试加载更多
|
||
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||
// 滚动到底部
|
||
window.scrollTo(0, scrollHeight);
|
||
|
||
// 等待新内容加载
|
||
await this.sleep(2000);
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('滚动加载被停止');
|
||
return false;
|
||
}
|
||
|
||
// 检查是否有新内容
|
||
const newScrollHeight = document.documentElement.scrollHeight;
|
||
return newScrollHeight > scrollHeight;
|
||
} else {
|
||
// 滚动到当前可见区域的底部
|
||
window.scrollBy(0, clientHeight);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 延时函数
|
||
sleep(ms) {
|
||
return new Promise(resolve => {
|
||
// 如果已经被停止,立即返回
|
||
if (!this.isAutoClicking) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const timeoutId = setTimeout(() => {
|
||
resolve();
|
||
// 清理定时器ID
|
||
this.cleanupTimers.delete(timeoutId);
|
||
}, ms);
|
||
|
||
// 添加到清理定时器集合
|
||
this.cleanupTimers.add(timeoutId);
|
||
|
||
// 创建一个检查停止的定时器
|
||
const checkStopInterval = setInterval(() => {
|
||
if (!this.isAutoClicking) {
|
||
clearTimeout(timeoutId);
|
||
clearInterval(checkStopInterval);
|
||
// 清理定时器ID
|
||
this.cleanupTimers.delete(timeoutId);
|
||
this.cleanupTimers.delete(checkStopInterval);
|
||
resolve();
|
||
}
|
||
}, 100);
|
||
|
||
// 添加到清理定时器集合
|
||
this.cleanupTimers.add(checkStopInterval);
|
||
});
|
||
}
|
||
|
||
// 评论间隔:最小~最大分钟之间随机等待,进度条实时倒计时
|
||
async randomCommentDelay() {
|
||
let min = 1;
|
||
let max = 2;
|
||
try {
|
||
const result = await chrome.storage.local.get(['commentDelayMin', 'commentDelayMax']);
|
||
const parsedMin = Number(result.commentDelayMin);
|
||
const parsedMax = Number(result.commentDelayMax);
|
||
if (Number.isFinite(parsedMin) && parsedMin >= 0) min = parsedMin;
|
||
if (Number.isFinite(parsedMax) && parsedMax >= 0) max = parsedMax;
|
||
} catch (error) {
|
||
console.log('读取评论间隔失败,使用默认 1~2 分钟:', error);
|
||
}
|
||
|
||
if (max < min) [min, max] = [max, min];
|
||
|
||
// 都是0则不等待
|
||
if (min === 0 && max === 0) return;
|
||
|
||
const totalSec = Math.round((min + Math.random() * (max - min)) * 60);
|
||
console.log(`评论间隔:随机等待 ${totalSec} 秒(范围 ${min}~${max} 分钟)`);
|
||
|
||
// 进度条提示实时倒计时
|
||
const tip = document.querySelector('.xhs-progress-tip');
|
||
let remain = totalSec;
|
||
const renderTip = () => {
|
||
if (tip) {
|
||
tip.textContent = remain > 0
|
||
? `⏳ 评论间隔:随机等待 ${remain} 秒后继续下一篇…`
|
||
: '💡 长按评论模式按钮可管理自定义评论';
|
||
}
|
||
};
|
||
renderTip();
|
||
const countdownTimer = setInterval(() => {
|
||
remain--;
|
||
renderTip();
|
||
}, 1000);
|
||
this.cleanupTimers.add(countdownTimer);
|
||
|
||
await this.sleep(totalSec * 1000);
|
||
|
||
clearInterval(countdownTimer);
|
||
this.cleanupTimers.delete(countdownTimer);
|
||
renderTip();
|
||
}
|
||
|
||
// 阻止笔记元素内的所有链接跳转
|
||
preventLinkNavigation(noteElement) {
|
||
const links = noteElement.querySelectorAll('a');
|
||
links.forEach(link => {
|
||
link.addEventListener('click', (event) => {
|
||
event.preventDefault(); // 阻止默认跳转
|
||
console.log('阻止链接跳转:', link.href);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 执行评论交互流程
|
||
async executeCommentInteraction() {
|
||
console.log('开始执行评论交互流程...');
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 1. 等待评论框出现
|
||
await this.waitForCommentBox();
|
||
console.log('评论框已出现');
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 2. 点击评论框
|
||
await this.clickCommentBox();
|
||
console.log('评论框已点击');
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 3. 等待输入框出现
|
||
await this.waitForTextarea();
|
||
console.log('输入框已出现');
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 4. 输入评论(图片模式改为上传图片)
|
||
let sentComment = '';
|
||
let sentImage = null;
|
||
if (this.commentMode === 'image') {
|
||
try {
|
||
sentImage = await this.uploadCommentImage();
|
||
sentComment = '[图片]';
|
||
console.log('评论图片已添加');
|
||
} catch (error) {
|
||
console.error('图片评论失败:', error);
|
||
// 只提示一次,避免每篇笔记都弹
|
||
if (!this.imageCommentWarned) {
|
||
this.imageCommentWarned = true;
|
||
this.showToast('⚠️ 图片评论发送失败:' + error.message + ';建议切换「自定义文字」或「AI」模式');
|
||
}
|
||
return; // 跳过本篇
|
||
}
|
||
} else {
|
||
sentComment = await this.inputComment();
|
||
console.log('评论已输入');
|
||
}
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 5. 提交评论
|
||
await this.submitComment();
|
||
console.log('评论已提交');
|
||
|
||
// 6. 校验是否发送成功并记录
|
||
const posted = await this.verifyCommentPosted(sentComment, this.commentMode === 'image', sentImage);
|
||
if (posted) {
|
||
await this.logCommentSuccess(sentComment, this.commentMode === 'image', sentImage);
|
||
} else {
|
||
console.warn('评论可能未发送成功,不计入记录');
|
||
this.showToast('⚠️ 评论可能未发送成功,本篇未记录');
|
||
}
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 7. 等待3秒
|
||
await this.sleep(3000);
|
||
console.log('等待3秒完成');
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 8. 按ESC键
|
||
await this.pressEscape();
|
||
console.log('ESC键已按下');
|
||
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('评论交互被停止');
|
||
return;
|
||
}
|
||
|
||
// 9. 等待评论框关闭
|
||
await this.waitForCommentBoxClose();
|
||
console.log('评论框已关闭,准备处理下一个笔记');
|
||
|
||
} catch (error) {
|
||
console.error('评论交互失败:', error);
|
||
}
|
||
}
|
||
|
||
// 等待评论框出现
|
||
async waitForCommentBox() {
|
||
console.log('等待评论框出现...');
|
||
return new Promise((resolve, reject) => {
|
||
let attempts = 0;
|
||
const maxAttempts = 60; // 最多等待30秒
|
||
|
||
const checkCommentBox = () => {
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
reject(new Error('等待评论框被停止'));
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
const commentBox = document.querySelector('#noteContainer > div.interaction-container > div.interactions.engage-bar > div > div > div.input-box > div.content-edit > div > div > span');
|
||
|
||
if (commentBox) {
|
||
console.log('评论框已找到:', commentBox);
|
||
resolve();
|
||
} else if (attempts >= maxAttempts) {
|
||
reject(new Error('等待评论框超时'));
|
||
} else {
|
||
setTimeout(checkCommentBox, 500);
|
||
}
|
||
};
|
||
|
||
checkCommentBox();
|
||
});
|
||
}
|
||
|
||
// 点击评论框
|
||
async clickCommentBox() {
|
||
const commentBox = document.querySelector('#noteContainer > div.interaction-container > div.interactions.engage-bar > div > div > div.input-box > div.content-edit > div > div > span');
|
||
|
||
if (commentBox) {
|
||
console.log('点击评论框:', commentBox);
|
||
commentBox.click();
|
||
await this.sleep(1000); // 等待点击生效
|
||
} else {
|
||
throw new Error('评论框元素未找到');
|
||
}
|
||
}
|
||
|
||
// 等待输入框出现
|
||
async waitForTextarea() {
|
||
console.log('等待输入框出现...');
|
||
return new Promise((resolve, reject) => {
|
||
let attempts = 0;
|
||
const maxAttempts = 30; // 最多等待15秒
|
||
|
||
const checkTextarea = () => {
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
reject(new Error('等待输入框被停止'));
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
const textarea = document.querySelector('#content-textarea');
|
||
|
||
if (textarea) {
|
||
console.log('输入框已找到:', textarea);
|
||
resolve();
|
||
} else if (attempts >= maxAttempts) {
|
||
reject(new Error('等待输入框超时'));
|
||
} else {
|
||
setTimeout(checkTextarea, 500);
|
||
}
|
||
};
|
||
|
||
checkTextarea();
|
||
});
|
||
}
|
||
|
||
// 输入评论
|
||
async inputComment() {
|
||
const textarea = document.querySelector('#content-textarea');
|
||
|
||
if (textarea) {
|
||
console.log(`开始生成评论,模式: ${this.commentMode}`);
|
||
|
||
try {
|
||
let commentContent = '';
|
||
|
||
if (this.commentMode === 'ai') {
|
||
// AI评论模式
|
||
commentContent = await this.generateAiComment();
|
||
} else if (this.commentMode === 'text') {
|
||
// 用户自定义文字模式
|
||
commentContent = await this.getUserTextComment();
|
||
} else {
|
||
// 固定评论池模式
|
||
commentContent = await this.getCustomComment();
|
||
}
|
||
|
||
if (!commentContent) {
|
||
throw new Error(this.commentMode === 'text'
|
||
? '自定义文字库为空,请在侧边栏填写'
|
||
: '无法生成评论内容');
|
||
}
|
||
|
||
// 聚焦到输入框
|
||
textarea.focus();
|
||
await this.sleep(500);
|
||
|
||
// 设置评论内容
|
||
textarea.textContent = commentContent;
|
||
|
||
// 触发输入事件
|
||
const inputEvent = new Event('input', { bubbles: true });
|
||
textarea.dispatchEvent(inputEvent);
|
||
|
||
console.log(`评论已输入 (${this.commentMode}):`, commentContent);
|
||
await this.sleep(1000); // 等待输入生效
|
||
return commentContent;
|
||
|
||
} catch (error) {
|
||
console.error('评论生成失败:', error);
|
||
// 如果评论生成失败,使用默认评论,并明确提示(避免误以为是AI生成的)
|
||
const defaultComment = '👍 赞!';
|
||
textarea.textContent = defaultComment;
|
||
const inputEvent = new Event('input', { bubbles: true });
|
||
textarea.dispatchEvent(inputEvent);
|
||
this.showToast(`评论生成失败(${this.getCommentModeLabel()}),已改用固定评论: ${error.message}`);
|
||
console.log('使用默认评论:', defaultComment);
|
||
return defaultComment;
|
||
}
|
||
} else {
|
||
throw new Error('输入框元素未找到');
|
||
}
|
||
}
|
||
|
||
// 获取用户自定义评论
|
||
async getCustomComment() {
|
||
try {
|
||
// 从存储中获取自定义评论列表
|
||
const result = await chrome.storage.local.get(['customComments']);
|
||
this.customComments = result.customComments || [];
|
||
|
||
if (this.customComments.length === 0) {
|
||
// 如果没有自定义评论,返回默认评论
|
||
return '👍 赞!';
|
||
}
|
||
|
||
// 循环使用评论列表
|
||
const comment = this.customComments[this.currentCommentIndex % this.customComments.length];
|
||
this.currentCommentIndex++;
|
||
|
||
console.log(`使用自定义评论 (${this.currentCommentIndex}/${this.customComments.length}):`, comment);
|
||
return comment;
|
||
|
||
} catch (error) {
|
||
console.error('获取自定义评论失败:', error);
|
||
return '👍 赞!';
|
||
}
|
||
}
|
||
|
||
// 获取用户自定义文字(text模式,一行一条循环发送)
|
||
async getUserTextComment() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['userComments']);
|
||
const userComments = (result.userComments || []).filter((line) => line && line.trim());
|
||
|
||
if (userComments.length === 0) {
|
||
throw new Error('自定义文字库为空,请在侧边栏填写并保存');
|
||
}
|
||
|
||
const comment = userComments[this.currentCommentIndex % userComments.length];
|
||
this.currentCommentIndex++;
|
||
|
||
console.log(`使用自定义文字 (${this.currentCommentIndex}/${userComments.length}):`, comment);
|
||
return comment;
|
||
} catch (error) {
|
||
console.error('获取自定义文字失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取评论模式的中文名称
|
||
getCommentModeLabel(mode = this.commentMode) {
|
||
const labels = {
|
||
custom: '固定评论池',
|
||
text: '自定义文字',
|
||
image: '图片回复',
|
||
ai: 'AI按笔记生成'
|
||
};
|
||
return labels[mode] || mode;
|
||
}
|
||
|
||
// 上传评论图片(image模式,多张图片循环发送)
|
||
async uploadCommentImage() {
|
||
const result = await chrome.storage.local.get(['commentImages']);
|
||
const images = result.commentImages || [];
|
||
|
||
if (images.length === 0) {
|
||
throw new Error('尚未在侧边栏上传评论图片');
|
||
}
|
||
|
||
const image = images[this.currentImageIndex % images.length];
|
||
this.currentImageIndex++;
|
||
console.log(`使用评论图片 (${this.currentImageIndex}/${images.length}):`, image.name || image.id);
|
||
|
||
// dataURL 转 File
|
||
const blob = await (await fetch(image.dataUrl)).blob();
|
||
const file = new File([blob], image.name || `comment-${Date.now()}.jpg`, {
|
||
type: blob.type || 'image/jpeg'
|
||
});
|
||
|
||
// 查找评论区的图片上传入口
|
||
let fileInput = this.findCommentFileInput();
|
||
if (!fileInput) {
|
||
fileInput = await this.revealCommentFileInput();
|
||
}
|
||
if (!fileInput) {
|
||
throw new Error('网页版评论框没有图片上传入口(小红书仅手机App支持图片评论)');
|
||
}
|
||
|
||
// 注入文件并触发 change 事件
|
||
const dataTransfer = new DataTransfer();
|
||
dataTransfer.items.add(file);
|
||
fileInput.files = dataTransfer.files;
|
||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
||
// 等待图片预览出现
|
||
await this.sleep(1500);
|
||
console.log('评论图片已注入');
|
||
return image;
|
||
}
|
||
|
||
// 在评论区查找文件上传 input
|
||
findCommentFileInput() {
|
||
const engageBar = document.querySelector('#noteContainer .engage-bar') || document.querySelector('.engage-bar');
|
||
if (engageBar) {
|
||
const scoped = engageBar.querySelector('input[type="file"]');
|
||
if (scoped) return scoped;
|
||
}
|
||
return document.querySelector('#noteContainer input[type="file"]')
|
||
|| document.querySelector('input[type="file"][accept*="image"]');
|
||
}
|
||
|
||
// 点击图片图标按钮,尝试唤出隐藏的 file input
|
||
async revealCommentFileInput() {
|
||
const engageBar = document.querySelector('#noteContainer .engage-bar') || document.querySelector('.engage-bar');
|
||
if (!engageBar) return null;
|
||
|
||
const candidates = engageBar.querySelectorAll(
|
||
'[class*="image"], [class*="img"], [class*="picture"], [class*="upload"], svg[class*="image"], svg[class*="img"]'
|
||
);
|
||
console.log(`尝试 ${candidates.length} 个候选图片按钮`);
|
||
|
||
for (const candidate of candidates) {
|
||
try {
|
||
// 点击图标外层可点击容器(通常 icon 的父级才有事件)
|
||
const clickable = candidate.closest('span, div, button, [role="button"]') || candidate;
|
||
clickable.click();
|
||
await this.sleep(600);
|
||
|
||
const input = this.findCommentFileInput();
|
||
if (input) {
|
||
console.log('已唤出文件上传入口:', clickable.className || clickable.tagName);
|
||
return input;
|
||
}
|
||
} catch (error) {
|
||
console.log('点击图片按钮失败:', error);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 校验评论是否发送成功(提交后延迟检查)
|
||
async verifyCommentPosted(commentText, isImage) {
|
||
await this.sleep(1500);
|
||
|
||
try {
|
||
if (isImage) {
|
||
// 图片模式:提交成功后输入区的图片预览会被清空
|
||
const preview = document.querySelector(
|
||
'#noteContainer .input-box img, .engage-bar .input-box [class*="upload"] img'
|
||
);
|
||
return !preview;
|
||
}
|
||
|
||
// 文字模式:评论框清空 或 评论区出现该文字,都算成功
|
||
const textarea = document.querySelector('#content-textarea');
|
||
const textareaEmpty = !textarea || !(textarea.textContent || '').trim();
|
||
|
||
const commentsContainer = document.querySelector('#noteContainer .comments-container')
|
||
|| document.querySelector('.comments-container');
|
||
const textVisible = !!(commentsContainer && commentText && commentsContainer.textContent.includes(commentText));
|
||
|
||
return textareaEmpty || textVisible;
|
||
} catch (error) {
|
||
console.log('校验评论结果失败,默认按成功处理:', error);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 记录一条成功的评论(写入 storage,侧边栏展示统计与列表)
|
||
async logCommentSuccess(commentText, isImage = false, image = null) {
|
||
try {
|
||
let url = '';
|
||
if (this.currentNoteData && this.currentNoteData.url) {
|
||
url = this.currentNoteData.url;
|
||
} else if (window.location.pathname.includes('/explore/')) {
|
||
url = window.location.href;
|
||
}
|
||
|
||
let title = this.currentNoteData && this.currentNoteData.title && this.currentNoteData.title !== '无标题'
|
||
? this.currentNoteData.title
|
||
: '';
|
||
if (!title) {
|
||
const titleEl = document.querySelector('#noteContainer .title')
|
||
|| document.querySelector('.note-detail-title')
|
||
|| document.querySelector('#noteContainer [class*="title"]');
|
||
title = titleEl ? titleEl.textContent.trim() : '';
|
||
}
|
||
if (!title) title = '无标题笔记';
|
||
|
||
const record = {
|
||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
url,
|
||
title: title.slice(0, 100),
|
||
comment: (commentText || '').slice(0, 200),
|
||
isImage: !!isImage,
|
||
mode: this.commentMode,
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
// 图片模式:存一张极小的缩略图方便侧边栏展示
|
||
if (isImage && image && image.dataUrl) {
|
||
try {
|
||
record.thumb = await this.makeImageThumb(image.dataUrl, 72);
|
||
} catch (error) {
|
||
console.log('生成评论缩略图失败:', error);
|
||
}
|
||
}
|
||
|
||
const result = await chrome.storage.local.get(['commentLogs']);
|
||
const logs = result.commentLogs || [];
|
||
logs.unshift(record);
|
||
if (logs.length > 300) logs.length = 300; // 最多保留300条
|
||
await chrome.storage.local.set({ commentLogs: logs });
|
||
console.log('已记录评论:', record.title, record.comment);
|
||
} catch (error) {
|
||
console.error('记录评论失败:', error);
|
||
}
|
||
}
|
||
|
||
// 生成小尺寸缩略图(dataURL)
|
||
async makeImageThumb(dataUrl, size = 72) {
|
||
const img = await new Promise((resolve, reject) => {
|
||
const image = new Image();
|
||
image.onload = () => resolve(image);
|
||
image.onerror = reject;
|
||
image.src = dataUrl;
|
||
});
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = size;
|
||
canvas.height = size;
|
||
const ctx = canvas.getContext('2d');
|
||
const scale = Math.max(size / img.width, size / img.height);
|
||
const w = img.width * scale;
|
||
const h = img.height * scale;
|
||
ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
|
||
return canvas.toDataURL('image/jpeg', 0.6);
|
||
}
|
||
|
||
// 生成AI评论(简化版,避免重复代码)
|
||
async generateAiComment() {
|
||
try {
|
||
// 获取笔记内容
|
||
const noteContent = await this.extractNoteContent();
|
||
console.log('提取的笔记内容:', noteContent);
|
||
|
||
// 使用AI生成评论
|
||
const aiComment = await this.generateAiCommentContent(noteContent);
|
||
console.log('AI生成的评论:', aiComment);
|
||
|
||
return aiComment;
|
||
|
||
} catch (error) {
|
||
console.error('AI评论生成失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 提取笔记内容
|
||
async extractNoteContent() {
|
||
// 尝试多种方式提取笔记内容
|
||
const contentSelectors = [
|
||
'.note-detail-desc',
|
||
'.note-content',
|
||
'.content-text',
|
||
'[class*="content"]',
|
||
'[class*="desc"]',
|
||
'.note-text'
|
||
];
|
||
|
||
let noteContent = '';
|
||
|
||
for (const selector of contentSelectors) {
|
||
const element = document.querySelector(selector);
|
||
if (element && element.textContent) {
|
||
noteContent = element.textContent.trim();
|
||
if (noteContent.length > 10) {
|
||
console.log('使用选择器提取内容:', selector);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有找到内容,尝试从页面标题获取
|
||
if (!noteContent || noteContent.length < 10) {
|
||
const titleElement = document.querySelector('h1, .title, [class*="title"]');
|
||
if (titleElement) {
|
||
noteContent = titleElement.textContent.trim();
|
||
console.log('从标题提取内容');
|
||
}
|
||
}
|
||
|
||
// 如果还是没有内容,使用页面URL作为内容
|
||
if (!noteContent || noteContent.length < 5) {
|
||
noteContent = window.location.href;
|
||
console.log('使用URL作为内容');
|
||
}
|
||
|
||
// 限制内容长度,避免内存溢出
|
||
const maxLength = 300;
|
||
if (noteContent.length > maxLength) {
|
||
noteContent = noteContent.substring(0, maxLength) + '...';
|
||
console.log(`内容过长,已截取前${maxLength}字符,避免内存溢出`);
|
||
}
|
||
|
||
console.log(`最终提取的内容长度: ${noteContent.length} 字符`);
|
||
return noteContent;
|
||
}
|
||
|
||
// 使用AI生成评论
|
||
async generateAiCommentContent(noteContent, retryCount = 0) {
|
||
try {
|
||
// 获取API密钥
|
||
const apiKey = await this.getApiKey();
|
||
if (!apiKey) {
|
||
throw new Error('未配置API密钥');
|
||
}
|
||
|
||
// 限制内容长度,避免内存溢出
|
||
const maxContentLength = 200; // 减少到200字符
|
||
let processedContent = noteContent;
|
||
|
||
if (processedContent.length > maxContentLength) {
|
||
processedContent = processedContent.substring(0, maxContentLength) + '...';
|
||
console.log(`内容过长,已截取前${maxContentLength}字符,避免内存溢出`);
|
||
}
|
||
|
||
// 构建AI提示词
|
||
const promptTemplate = (await chrome.storage.local.get(['aiPrompt'])).aiPrompt || `根据笔记内容来生成符合笔记的评论。最后只需要输出评论内容,不要输出你的推理,不要输出任何与评论无关的内容,不要输出#相关的内容,不要输出笔记关键词相关的内容。评论不要超过300字。这个是小红书的笔记内容:\n\n{{content}}\n\n`;
|
||
const prompt = promptTemplate.replace(/\{\{\s*content\s*\}\}/g, processedContent);
|
||
|
||
console.log('发送AI请求,提示词长度:', prompt.length, '字符');
|
||
|
||
// 读取自定义模型配置,实际请求交给后台脚本执行(规避页面 CORS / 混合内容限制)
|
||
const modelConfig = await chrome.storage.local.get(['apiBaseUrl', 'modelName']);
|
||
|
||
console.log('发送AI请求到后台,模型:', modelConfig.modelName || '(默认)', '提示词长度:', prompt.length, '字符');
|
||
|
||
const apiResult = await chrome.runtime.sendMessage({
|
||
action: 'callAiApi',
|
||
apiKey: apiKey,
|
||
baseUrl: modelConfig.apiBaseUrl,
|
||
model: modelConfig.modelName,
|
||
prompt: prompt,
|
||
maxTokens: 300
|
||
});
|
||
|
||
if (!apiResult || !apiResult.ok) {
|
||
const errorMessage = (apiResult && apiResult.error) || '后台服务未响应,请在浏览器扩展页重新加载插件';
|
||
|
||
// 如果是内存溢出错误,尝试重试
|
||
if (errorMessage.includes('out of memory') && retryCount < this.maxRetries) {
|
||
console.log(`内存溢出,尝试截取更短内容重试 (${retryCount + 1}/${this.maxRetries + 1})`);
|
||
// 截取更短的内容重试
|
||
const shorterContent = processedContent.substring(0, 100) + '...';
|
||
return await this.generateAiCommentContent(shorterContent, retryCount + 1);
|
||
}
|
||
|
||
throw new Error(`AI API调用失败: ${errorMessage}`);
|
||
}
|
||
|
||
const aiComment = String(apiResult.content || '').trim();
|
||
|
||
if (!aiComment) {
|
||
throw new Error('AI返回的评论内容为空');
|
||
}
|
||
|
||
console.log('AI生成评论成功:', aiComment, '| 模型:', apiResult.model, '| 端点:', apiResult.url);
|
||
return aiComment;
|
||
|
||
} catch (error) {
|
||
console.error('AI评论生成失败:', error);
|
||
|
||
// 如果是内存溢出且还有重试机会,尝试重试
|
||
if (error.message.includes('out of memory') && retryCount < this.maxRetries) {
|
||
console.log(`内存溢出,尝试截取更短内容重试 (${retryCount + 1}/${this.maxRetries + 1})`);
|
||
const shorterContent = noteContent.substring(0, 50) + '...';
|
||
return await this.generateAiCommentContent(shorterContent, retryCount + 1);
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取API密钥
|
||
async getApiKey() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['apiKey']);
|
||
return result.apiKey || '';
|
||
} catch (error) {
|
||
console.log('获取API密钥失败:', error);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
// 提交评论
|
||
async submitComment() {
|
||
const submitButton = document.querySelector('#noteContainer > div.interaction-container > div.interactions.engage-bar > div > div > div.bottom > div > div.right-btn-area > button.btn.submit');
|
||
|
||
if (submitButton) {
|
||
console.log('点击提交按钮:', submitButton);
|
||
submitButton.click();
|
||
await this.sleep(2000); // 等待提交完成
|
||
} else {
|
||
throw new Error('提交按钮未找到');
|
||
}
|
||
}
|
||
|
||
// 按ESC键
|
||
async pressEscape() {
|
||
console.log('模拟按ESC键...');
|
||
|
||
// 创建ESC键事件
|
||
const keydownEvent = new KeyboardEvent('keydown', {
|
||
key: 'Escape',
|
||
code: 'Escape',
|
||
keyCode: 27,
|
||
which: 27,
|
||
bubbles: true,
|
||
cancelable: true
|
||
});
|
||
|
||
const keyupEvent = new KeyboardEvent('keyup', {
|
||
key: 'Escape',
|
||
code: 'Escape',
|
||
keyCode: 27,
|
||
which: 27,
|
||
bubbles: true,
|
||
cancelable: true
|
||
});
|
||
|
||
// 在document上触发ESC键事件
|
||
document.dispatchEvent(keydownEvent);
|
||
document.dispatchEvent(keyupEvent);
|
||
|
||
console.log('ESC键事件已触发');
|
||
await this.sleep(500);
|
||
}
|
||
|
||
// 等待评论框关闭
|
||
async waitForCommentBoxClose() {
|
||
console.log('等待评论框关闭...');
|
||
return new Promise((resolve) => {
|
||
let attempts = 0;
|
||
const maxAttempts = 20; // 最多等待10秒
|
||
|
||
const checkCommentBoxClosed = () => {
|
||
// 检查是否被停止
|
||
if (!this.isAutoClicking) {
|
||
console.log('等待评论框关闭被停止');
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
const textarea = document.querySelector('#content-textarea');
|
||
|
||
if (!textarea) {
|
||
console.log('评论框已关闭');
|
||
resolve();
|
||
} else if (attempts >= maxAttempts) {
|
||
console.log('等待评论框关闭超时,继续执行');
|
||
resolve();
|
||
} else {
|
||
setTimeout(checkCommentBoxClosed, 500);
|
||
}
|
||
};
|
||
|
||
checkCommentBoxClosed();
|
||
});
|
||
}
|
||
|
||
// 执行内存清理
|
||
performMemoryCleanup() {
|
||
console.log('执行内存清理...');
|
||
|
||
// 清理高亮元素
|
||
this.cleanupHighlightedElements();
|
||
|
||
// 清理已点击的标记
|
||
const clickedElements = document.querySelectorAll('[data-clicked="true"]');
|
||
clickedElements.forEach(el => {
|
||
delete el.dataset.clicked;
|
||
});
|
||
|
||
// 强制垃圾回收(如果浏览器支持)
|
||
if (window.gc) {
|
||
try {
|
||
window.gc();
|
||
console.log('垃圾回收已执行');
|
||
} catch (e) {
|
||
console.log('垃圾回收失败:', e);
|
||
}
|
||
}
|
||
|
||
console.log('内存清理完成');
|
||
}
|
||
|
||
// 页面级别的内存泄漏防护
|
||
setupMemoryLeakProtection() {
|
||
// 监听页面错误
|
||
window.addEventListener('error', (event) => {
|
||
if (event.error && event.error.message.includes('out of memory')) {
|
||
console.error('检测到内存溢出错误,执行紧急清理');
|
||
this.emergencyMemoryCleanup();
|
||
}
|
||
});
|
||
|
||
// 监听未处理的Promise拒绝
|
||
window.addEventListener('unhandledrejection', (event) => {
|
||
if (event.reason && event.reason.message && event.reason.message.includes('out of memory')) {
|
||
console.error('检测到未处理的Promise内存错误,执行紧急清理');
|
||
this.emergencyMemoryCleanup();
|
||
}
|
||
});
|
||
|
||
// 监听页面性能警告
|
||
if ('PerformanceObserver' in window) {
|
||
try {
|
||
const observer = new PerformanceObserver((list) => {
|
||
for (const entry of list.getEntries()) {
|
||
if (entry.entryType === 'measure' && entry.name.includes('memory')) {
|
||
console.log('性能测量:', entry);
|
||
}
|
||
}
|
||
});
|
||
observer.observe({ entryTypes: ['measure'] });
|
||
} catch (error) {
|
||
console.log('性能观察器设置失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 调试信息输出
|
||
logDebugInfo() {
|
||
console.log('=== 内存管理调试信息 ===');
|
||
console.log('当前状态:', {
|
||
isAutoClicking: this.isAutoClicking,
|
||
processingNotesCount: this.processingNotes.size,
|
||
cleanupTimersCount: this.cleanupTimers.size,
|
||
memoryThreshold: `${(this.memoryThreshold / 1024 / 1024).toFixed(2)}MB`
|
||
});
|
||
|
||
const memoryStats = this.getMemoryStats();
|
||
if (memoryStats) {
|
||
console.log('内存统计:', memoryStats);
|
||
}
|
||
|
||
console.log('DOM节点数量:', document.querySelectorAll('*').length);
|
||
console.log('高亮元素数量:', document.querySelectorAll('[style*="outline: 3px solid #4caf50"]').length);
|
||
console.log('========================');
|
||
}
|
||
|
||
// 切换评论模式(固定评论池 → 自定义文字 → 图片回复 → AI → 循环)
|
||
async toggleCommentMode() {
|
||
const modes = ['custom', 'text', 'image', 'ai'];
|
||
const currentIndex = modes.indexOf(this.commentMode);
|
||
this.commentMode = modes[(currentIndex + 1) % modes.length];
|
||
console.log(`切换评论模式为: ${this.commentMode}`);
|
||
|
||
// 保存设置到存储
|
||
try {
|
||
await chrome.storage.local.set({ commentMode: this.commentMode });
|
||
console.log('评论模式设置已保存');
|
||
} catch (error) {
|
||
console.error('保存评论模式设置失败:', error);
|
||
}
|
||
|
||
this.updateCommentModeButton();
|
||
this.showToast(`评论模式已切换为: ${this.getCommentModeLabel()}`);
|
||
}
|
||
|
||
// 更新评论模式按钮显示
|
||
updateCommentModeButton() {
|
||
const commentModeBtn = document.getElementById('xhs-comment-mode');
|
||
if (commentModeBtn) {
|
||
const icons = { custom: '📋', text: '📝', image: '🖼️', ai: '🤖' };
|
||
commentModeBtn.textContent = `${icons[this.commentMode] || '💬'} ${this.getCommentModeLabel()}`;
|
||
if (this.commentMode === 'ai') {
|
||
commentModeBtn.classList.remove('xhs-secondary-btn');
|
||
commentModeBtn.classList.add('xhs-primary-btn');
|
||
} else {
|
||
commentModeBtn.classList.remove('xhs-primary-btn');
|
||
commentModeBtn.classList.add('xhs-secondary-btn');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加自定义评论
|
||
async addCustomComment(comment) {
|
||
try {
|
||
if (!comment || comment.trim().length === 0) {
|
||
throw new Error('评论内容不能为空');
|
||
}
|
||
|
||
const result = await chrome.storage.local.get(['customComments']);
|
||
const customComments = result.customComments || [];
|
||
|
||
// 检查是否已存在
|
||
if (customComments.includes(comment.trim())) {
|
||
throw new Error('该评论已存在');
|
||
}
|
||
|
||
// 添加新评论
|
||
customComments.push(comment.trim());
|
||
|
||
// 保存到存储
|
||
await chrome.storage.local.set({ customComments });
|
||
|
||
// 更新本地列表
|
||
this.customComments = customComments;
|
||
|
||
console.log('自定义评论已添加:', comment);
|
||
this.showToast('自定义评论已添加');
|
||
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('添加自定义评论失败:', error);
|
||
this.showToast(`添加失败: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 删除自定义评论
|
||
async removeCustomComment(comment) {
|
||
try {
|
||
const result = await chrome.storage.local.get(['customComments']);
|
||
let customComments = result.customComments || [];
|
||
|
||
// 移除指定评论
|
||
customComments = customComments.filter(c => c !== comment);
|
||
|
||
// 保存到存储
|
||
await chrome.storage.local.set({ customComments });
|
||
|
||
// 更新本地列表
|
||
this.customComments = customComments;
|
||
|
||
console.log('自定义评论已删除:', comment);
|
||
this.showToast('自定义评论已删除');
|
||
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('删除自定义评论失败:', error);
|
||
this.showToast(`删除失败: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 获取所有自定义评论
|
||
async getAllCustomComments() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['customComments']);
|
||
return result.customComments || [];
|
||
} catch (error) {
|
||
console.error('获取自定义评论失败:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 显示自定义评论管理弹窗
|
||
showCustomCommentModal() {
|
||
// 创建弹窗
|
||
const modal = document.createElement('div');
|
||
modal.className = 'xhs-comment-modal';
|
||
|
||
modal.innerHTML = `
|
||
<div class="xhs-comment-modal-content">
|
||
<h3>自定义评论管理</h3>
|
||
<div class="xhs-comment-input-area">
|
||
<input type="text" class="xhs-comment-input" placeholder="输入新评论内容...">
|
||
<button class="xhs-add-comment-btn">添加评论</button>
|
||
</div>
|
||
<div class="xhs-comment-list">
|
||
<div class="xhs-comment-list-header">已添加的评论:</div>
|
||
<div class="xhs-comment-items"></div>
|
||
</div>
|
||
<div class="xhs-modal-actions">
|
||
<button class="xhs-modal-btn xhs-secondary-btn" id="close-comment-modal">关闭</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 绑定事件
|
||
this.bindCustomCommentModalEvents(modal);
|
||
|
||
// 加载现有评论
|
||
this.loadCustomCommentsToModal(modal);
|
||
}
|
||
|
||
// 绑定自定义评论弹窗事件
|
||
bindCustomCommentModalEvents(modal) {
|
||
const addBtn = modal.querySelector('.xhs-add-comment-btn');
|
||
const input = modal.querySelector('.xhs-comment-input');
|
||
const closeBtn = modal.querySelector('#close-comment-modal');
|
||
|
||
// 添加评论
|
||
addBtn.addEventListener('click', async () => {
|
||
const comment = input.value.trim();
|
||
if (comment) {
|
||
const success = await this.addCustomComment(comment);
|
||
if (success) {
|
||
input.value = '';
|
||
this.loadCustomCommentsToModal(modal);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 回车添加评论
|
||
input.addEventListener('keypress', async (e) => {
|
||
if (e.key === 'Enter') {
|
||
const comment = input.value.trim();
|
||
if (comment) {
|
||
const success = await this.addCustomComment(comment);
|
||
if (success) {
|
||
input.value = '';
|
||
this.loadCustomCommentsToModal(modal);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 关闭弹窗
|
||
closeBtn.addEventListener('click', () => {
|
||
modal.remove();
|
||
});
|
||
|
||
// 点击背景关闭
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 加载自定义评论到弹窗
|
||
async loadCustomCommentsToModal(modal) {
|
||
const commentItems = modal.querySelector('.xhs-comment-items');
|
||
const comments = await this.getAllCustomComments();
|
||
|
||
if (comments.length === 0) {
|
||
commentItems.innerHTML = '<div class="xhs-no-comments">暂无自定义评论</div>';
|
||
return;
|
||
}
|
||
|
||
commentItems.innerHTML = comments.map(comment => `
|
||
<div class="xhs-comment-item">
|
||
<span class="xhs-comment-text">${comment}</span>
|
||
<button class="xhs-delete-comment-btn" data-comment="${comment}">删除</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 绑定删除按钮事件
|
||
const deleteBtns = modal.querySelectorAll('.xhs-delete-comment-btn');
|
||
deleteBtns.forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
const comment = btn.dataset.comment;
|
||
const success = await this.removeCustomComment(comment);
|
||
if (success) {
|
||
this.loadCustomCommentsToModal(modal);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new XHSNoteSaver();
|
||
});
|
||
} else {
|
||
new XHSNoteSaver();
|
||
} |