Files
xhs-auto-comment/content.js
T
88531 85b7d6f762 fix: 明确图片评论限制 v1.7.7
- 小红书网页版评论框不支持图片(仅App支持),发送失败时页面弹提示(只弹一次)
- 侧边栏图片模式显示橙色警告,引导改用自定义文字/AI模式
2026-10-10 16:01:14 +08:00

2586 lines
97 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 内容脚本 - 在小红书页面上添加收藏功能
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();
}