// 内容脚本 - 在小红书页面上添加收藏功能
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' / 'ai'
this.customComments = []; // 固定评论池
this.currentCommentIndex = 0; // 当前评论索引
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 === 'image' ? 'custom' : 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 === 'image' ? 'custom' : 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 = `
选择标签
${tags.map(tag => `
${tag.name} (${tag.count || 0})
`).join('')}
`;
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 = `
🤖 自动点击进行中...
已点击: 0
总计: 0
内存: --
💡 长按评论模式按钮可管理自定义评论
`;
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. 输入评论
const 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);
if (posted) {
await this.logCommentSuccess(sentComment);
// 试用额度计数(授权用户不计);额度用完自动停止
await this.checkUsageQuota();
} 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: '自定义文字',
ai: 'AI按笔记生成'
};
return labels[mode] || mode;
}
// 校验评论是否发送成功(提交后延迟检查)
async verifyCommentPosted(commentText) {
await this.sleep(1500);
try {
// 评论框清空 或 评论区出现该文字,都算成功
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;
}
}
// 试用额度:每成功一条让后台计数;allowed=false 时立即停止本轮任务
async checkUsageQuota() {
try {
const res = await chrome.runtime.sendMessage({ action: 'usageTick' });
const d = res && res.data;
if (!d) return;
if (d.allowed === false) {
this.isAutoClicking = false;
this.cleanup();
this.showToast('⛔ ' + (d.message || '体验额度已用完,已自动停止'));
console.log('额度用完,已停止:', d);
return;
}
if (d.state === 'trial' && typeof d.quotaLeft === 'number' && d.quotaLeft > 0 && d.quotaLeft <= 10) {
this.showToast(`试用额度还剩 ${d.quotaLeft} 条`);
} else if (d.state === 'grace' && typeof d.dayLeft === 'number' && d.dayLeft > 0) {
console.log(`体验模式:今日剩余 ${d.dayLeft} 条`);
}
} catch (error) {
console.log('额度检查失败(不阻断):', error);
}
}
// 记录一条成功的评论(写入 storage,侧边栏展示统计与列表)
async logCommentSuccess(commentText) {
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),
mode: this.commentMode,
timestamp: Date.now()
};
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);
}
}
// 生成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', '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: '📝', 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 = `
`;
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 = '';
return;
}
commentItems.innerHTML = comments.map(comment => `
`).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();
}
自定义评论管理