Files
xhs-auto-comment/sidepanel.js
T
88531 2e97e62210 feat: 评论间隔随机化 v1.7.6
- 每条评论之间在设定范围内随机等待(默认1~2分钟,可自定义,支持小数)
- 进度条实时倒计时显示剩余秒数,可随时停止
- 填0则不等待
2026-10-10 15:14:19 +08:00

1334 lines
53 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.
// 默认自定义模型配置(OpenAI 兼容接口)
const DEFAULT_AI = {
apiBaseUrl: 'https://ai.88531.cn/v1',
model: 'mimo-v2.6-flash'
};
// 弹窗界面功能
class AutoCommentManager {
constructor() {
this.init();
}
async init() {
await this.loadClickMode(); // 加载点击模式设置
await this.loadDelayConfig(); // 加载评论间隔设置
await this.loadRegion(); // 加载按地区搜索设置
await this.loadApiKey(); // 加载API密钥
await this.loadModelConfig(); // 加载自定义模型配置
await this.loadCommentMode(); // 加载评论方式(固定评论池 / 自定义文字 / 图片 / AI)
await this.loadAiPrompt(); // 加载AI提示词
await this.loadUserComments(); // 加载自定义文字库
await this.loadImagePreviews(); // 加载已上传的评论图片
this.bindEvents();
this.updateAiStatus();
this.loadCommentLogs(); // 加载评论记录
this.initAuth(); // 授权状态 / 公告 / 更新提示
// 小红书页面记录新评论后自动刷新列表
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' && changes.commentLogs) {
this.renderCommentLogs(changes.commentLogs.newValue || []);
}
});
}
bindEvents() {
// 搜索功能
document.getElementById('search-btn').addEventListener('click', () => {
this.searchXiaohongshu();
});
document.getElementById('keyword-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.searchXiaohongshu();
}
});
// 按地区搜索
const regionSelect = document.getElementById('region-select');
if (regionSelect) {
regionSelect.addEventListener('change', (e) => {
this.onRegionChange(e.target.value);
});
}
const regionCustom = document.getElementById('region-custom');
if (regionCustom) {
regionCustom.addEventListener('input', (e) => {
this.saveRegion({ regionCustom: e.target.value });
});
regionCustom.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.searchXiaohongshu();
}
});
}
// 自动点击功能
document.getElementById('auto-click-btn').addEventListener('click', () => {
this.triggerAutoClick();
});
// 测试元素检测功能
document.getElementById('test-detection-btn').addEventListener('click', () => {
this.testElementDetection();
});
// 点击模式选择器
document.getElementById('expand-content').addEventListener('change', (e) => {
this.saveClickMode(e.target.checked);
});
// 评论间隔:输入即保存
['delay-min', 'delay-max'].forEach((id) => {
const input = document.getElementById(id);
if (input) {
input.addEventListener('input', () => {
this.saveDelayConfig();
});
}
});
// 评论方式选择(固定评论池 / 自定义文字 / 图片 / AI 按笔记生成)
document.querySelectorAll('input[name="comment-mode"]').forEach((radio) => {
radio.addEventListener('change', (e) => {
this.saveCommentMode(e.target.value);
});
});
// 自定义文字库:输入即保存
const userCommentsArea = document.getElementById('user-comments');
if (userCommentsArea) {
userCommentsArea.addEventListener('input', () => {
this.saveUserComments();
});
}
const saveUserCommentsBtn = document.getElementById('save-user-comments-btn');
if (saveUserCommentsBtn) {
saveUserCommentsBtn.addEventListener('click', () => {
this.saveUserComments(true);
});
}
// 图片回复:选择图片即压缩保存
const imageInput = document.getElementById('comment-image-input');
if (imageInput) {
imageInput.addEventListener('change', (e) => {
this.handleImageFiles(e.target.files);
imageInput.value = ''; // 允许重复选择同一文件
});
}
// 评论记录:手动刷新
const refreshLogBtn = document.getElementById('refresh-log-btn');
if (refreshLogBtn) {
refreshLogBtn.addEventListener('click', () => {
this.loadCommentLogs();
});
}
// 授权:卡密激活
const activateBtn = document.getElementById('activate-btn');
if (activateBtn) {
activateBtn.addEventListener('click', () => {
this.activateCard();
});
}
const cardInput = document.getElementById('card-key-input');
if (cardInput) {
cardInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.activateCard();
});
}
// 授权:购买(展开支付方式选择)
const buyBtn = document.getElementById('buy-btn');
if (buyBtn) {
buyBtn.addEventListener('click', () => {
const row = document.getElementById('pay-select-row');
if (row) row.hidden = !row.hidden;
});
}
const payCancelBtn = document.getElementById('pay-cancel-btn');
if (payCancelBtn) {
payCancelBtn.addEventListener('click', () => {
const row = document.getElementById('pay-select-row');
if (row) row.hidden = true;
});
}
document.querySelectorAll('.pay-btn[data-pay]').forEach((btn) => {
btn.addEventListener('click', () => {
this.startBuyFlow(btn.dataset.pay);
});
});
// API密钥配置
document.getElementById('api-key').addEventListener('input', (e) => {
this.saveApiKey(e.target.value);
this.updateAiStatus();
});
// 自定义模型:API地址与模型名,输入即保存
const baseUrlInput = document.getElementById('api-base-url');
if (baseUrlInput) {
baseUrlInput.addEventListener('input', (e) => {
this.saveModelConfig({ apiBaseUrl: e.target.value });
});
baseUrlInput.addEventListener('change', () => this.updateAiStatus());
}
const modelInput = document.getElementById('model-name');
if (modelInput) {
modelInput.addEventListener('input', (e) => {
this.saveModelConfig({ modelName: e.target.value });
});
modelInput.addEventListener('change', () => this.updateAiStatus());
}
// 测试AI按钮
document.getElementById('test-ai-btn').addEventListener('click', () => {
this.testAiGeneration();
});
// AI提示词保存
const savePromptBtn = document.getElementById('save-prompt-btn');
if (savePromptBtn) {
savePromptBtn.addEventListener('click', () => {
this.saveAiPrompt();
});
}
// AI提示词恢复默认
const resetPromptBtn = document.getElementById('reset-prompt-btn');
if (resetPromptBtn) {
resetPromptBtn.addEventListener('click', () => {
this.resetAiPromptToDefault();
});
}
}
async searchXiaohongshu() {
if (!(await this.isFeatureAllowed())) return;
const keyword = document.getElementById('keyword-input').value.trim();
if (!keyword) {
alert('请输入搜索关键词');
return;
}
// 按地区搜索:地区拼进关键词(如「咖啡 上海」)
const region = this.getSearchRegion();
const finalKeyword = region ? `${keyword} ${region}` : keyword;
// 构建小红书搜索URL
const encodedKeyword = encodeURIComponent(finalKeyword);
const searchUrl = `https://www.xiaohongshu.com/search_result?keyword=${encodedKeyword}`;
console.log('准备打开搜索页面:', searchUrl);
// 在新标签页打开搜索结果
chrome.tabs.create({ url: searchUrl }, (tab) => {
console.log('新标签页已创建:', tab.id);
if (chrome.runtime.lastError) {
console.error('创建标签页失败:', chrome.runtime.lastError);
return;
}
// 等待页面加载完成后发送自动点击消息
chrome.tabs.onUpdated.addListener(function listener(tabId, changeInfo) {
console.log('标签页更新:', tabId, changeInfo);
if (tabId === tab.id && changeInfo.status === 'complete') {
console.log('页面加载完成,准备发送自动点击消息');
// 页面加载完成,延迟一下确保DOM完全渲染
setTimeout(() => {
console.log('发送自动点击消息到标签页:', tab.id);
chrome.tabs.sendMessage(tab.id, {
action: 'autoClickNotes',
keyword: finalKeyword
}, (response) => {
if (chrome.runtime.lastError) {
console.error('发送消息失败:', chrome.runtime.lastError);
} else {
console.log('消息发送成功,响应:', response);
}
});
}, 3000); // 增加延迟时间
// 移除监听器
chrome.tabs.onUpdated.removeListener(listener);
}
});
});
// 清空输入框
document.getElementById('keyword-input').value = '';
// 侧边栏常驻不关闭,改为在面板内显示状态
this.showActionStatus(true, region
? `✓ 已按地区「${region}」打开搜索页,稍后自动开始评论流程`
: '✓ 已打开搜索页,稍后自动开始评论流程');
}
// 获取当前选择的地区(空字符串 = 不限)
getSearchRegion() {
const select = document.getElementById('region-select');
if (!select) return '';
if (select.value === '__custom__') {
return (document.getElementById('region-custom')?.value || '').trim();
}
return select.value;
}
// 地区下拉切换(含自定义输入框显隐)
onRegionChange(value) {
const customInput = document.getElementById('region-custom');
const isCustom = value === '__custom__';
if (customInput) {
customInput.hidden = !isCustom;
if (isCustom) customInput.focus();
}
this.saveRegion({ regionSelectVal: value });
}
// 保存地区设置
async saveRegion(changes) {
try {
await chrome.storage.local.set(changes);
console.log('保存地区设置:', Object.keys(changes).join(', '));
} catch (error) {
console.error('保存地区设置失败:', error);
}
}
// 加载地区设置
async loadRegion() {
try {
const result = await chrome.storage.local.get(['regionSelectVal', 'regionCustom']);
const select = document.getElementById('region-select');
const customInput = document.getElementById('region-custom');
if (!select) return;
const saved = result.regionSelectVal || '';
const hasOption = Array.from(select.options).some((o) => o.value === saved);
const value = hasOption ? saved : '';
select.value = value;
const isCustom = value === '__custom__';
if (customInput) {
customInput.value = result.regionCustom || '';
customInput.hidden = !isCustom;
}
console.log('加载地区设置:', value, result.regionCustom || '');
} catch (error) {
console.error('加载地区设置失败:', error);
}
}
// 面板内联状态提示(替代弹窗关闭/alert)
showActionStatus(ok, text) {
const el = document.getElementById('action-status');
if (!el) return;
el.textContent = text;
el.className = 'action-status ' + (ok ? 'ok' : 'fail');
}
// 手动触发自动点击功能
async triggerAutoClick() {
try {
if (!(await this.isFeatureAllowed())) return;
// 获取当前活动标签页
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) {
alert('无法获取当前标签页');
return;
}
// 检查是否在小红书页面
if (!tab.url || !tab.url.includes('xiaohongshu.com')) {
alert('请在小红书页面上使用此功能');
return;
}
// 发送自动点击消息
await chrome.tabs.sendMessage(tab.id, {
action: 'autoClickNotes'
});
this.showActionStatus(true, '✓ 已开始自动点击笔记');
} catch (error) {
console.error('触发自动点击失败:', error);
this.showActionStatus(false, '✗ 触发失败: ' + error.message);
}
}
// 测试元素检测功能
async testElementDetection() {
try {
// 获取当前活动标签页
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) {
alert('无法获取当前标签页');
return;
}
// 检查是否在小红书页面
if (!tab.url || !tab.url.includes('xiaohongshu.com')) {
alert('请在小红书页面上使用此功能');
return;
}
console.log('发送测试元素检测消息到标签页:', tab.id);
// 发送测试消息
const response = await chrome.tabs.sendMessage(tab.id, {
action: 'testElementDetection'
});
if (response && response.success) {
let message = `元素检测结果:\n\n主要选择器: ${response.primarySelector}\n主要选择器找到: ${response.primaryElementsCount} 个元素\n\n总元素: ${response.totalElements}\n可见元素: ${response.visibleElements}`;
// 添加第一个元素的详细分析
if (response.firstElementAnalysis) {
const analysis = response.firstElementAnalysis;
message += `\n\n第一个元素分析:\n- 标签: ${analysis.tagName}.${analysis.className}`;
message += `\n- 笔记索引: ${analysis.noteIndex || '未知'}`;
message += `\n- 选择器: ${analysis.selector || '未知'}`;
// 显示目标链接检测结果
if (analysis.targetLink) {
if (analysis.targetLink.found) {
message += `\n- 目标链接: ✅ 已找到`;
message += `\n - 链接: ${analysis.targetLink.href}`;
message += `\n - 文本: ${analysis.targetLink.text}`;
message += `\n - 类名: ${analysis.targetLink.className}`;
} else {
message += `\n- 目标链接: ❌ ${analysis.targetLink.message}`;
}
}
if (analysis.links && analysis.links.length > 0) {
message += `\n- 所有链接 (${analysis.links.length}个):`;
analysis.links.forEach((link, index) => {
message += `\n ${index + 1}. ${link.text} (${link.href})`;
});
}
if (analysis.buttons && analysis.buttons.length > 0) {
message += `\n- 按钮 (${analysis.buttons.length}个):`;
analysis.buttons.forEach((btn, index) => {
message += `\n ${index + 1}. ${btn.text} (${btn.className})`;
});
}
if (analysis.clickableDivs && analysis.clickableDivs.length > 0) {
message += `\n- 可点击div (${analysis.clickableDivs.length}个):`;
analysis.clickableDivs.forEach((div, index) => {
message += `\n ${index + 1}. ${div.text} (${div.className})`;
});
}
}
message += `\n\n前5个元素详情:\n${response.elementDetails.map(el =>
`- ${el.tagName}.${el.className} (链接:${el.hasLink}, 图片:${el.hasImage}, 文本长度:${el.textLength})`
).join('\n')}`;
alert(message);
} else {
alert('测试失败,请检查控制台');
}
} catch (error) {
console.error('测试元素检测失败:', error);
alert('测试失败: ' + error.message);
}
}
// 保存用户选择的点击模式
async saveClickMode(shouldExpand) {
try {
await chrome.storage.local.set({ expandContent: shouldExpand });
console.log('保存点击模式:', shouldExpand);
} catch (error) {
console.error('保存点击模式失败:', error);
}
}
// 加载用户选择的点击模式
async loadClickMode() {
try {
const result = await chrome.storage.local.get(['expandContent']);
const shouldExpand = result.expandContent !== false; // 默认展开内容
document.getElementById('expand-content').checked = shouldExpand;
console.log('加载点击模式:', shouldExpand);
} catch (error) {
console.error('加载点击模式失败:', error);
}
}
// 加载评论间隔(分钟,默认 1~2 分钟随机)
async loadDelayConfig() {
try {
const result = await chrome.storage.local.get(['commentDelayMin', 'commentDelayMax']);
const min = Number(result.commentDelayMin);
const max = Number(result.commentDelayMax);
const minInput = document.getElementById('delay-min');
const maxInput = document.getElementById('delay-max');
if (minInput) minInput.value = Number.isFinite(min) && min >= 0 ? min : 1;
if (maxInput) maxInput.value = Number.isFinite(max) && max >= 0 ? max : 2;
console.log('加载评论间隔:', minInput?.value, '~', maxInput?.value, '分钟');
} catch (error) {
console.error('加载评论间隔失败:', error);
}
}
// 保存评论间隔
async saveDelayConfig() {
try {
const minInput = document.getElementById('delay-min');
const maxInput = document.getElementById('delay-max');
let min = Number(minInput?.value);
let max = Number(maxInput?.value);
if (!Number.isFinite(min) || min < 0) min = 1;
if (!Number.isFinite(max) || max < 0) max = 2;
if (max < min) {
// 自动纠正顺序
[min, max] = [max, min];
if (minInput) minInput.value = min;
if (maxInput) maxInput.value = max;
}
await chrome.storage.local.set({ commentDelayMin: min, commentDelayMax: max });
console.log('保存评论间隔:', min, '~', max, '分钟');
} catch (error) {
console.error('保存评论间隔失败:', error);
}
}
// 保存API密钥
async saveApiKey(apiKey) {
try {
await chrome.storage.local.set({ apiKey: apiKey });
console.log('保存API密钥:', apiKey);
} catch (error) {
console.error('保存API密钥失败:', error);
}
}
// 加载API密钥
async loadApiKey() {
try {
const result = await chrome.storage.local.get(['apiKey']);
const apiKey = result.apiKey || ''; // 默认API密钥
document.getElementById('api-key').value = apiKey;
console.log('加载API密钥:', apiKey);
} catch (error) {
console.error('加载API密钥失败:', error);
}
}
// 加载评论方式(custom / text / image / ai)
async loadCommentMode() {
try {
const result = await chrome.storage.local.get(['commentMode']);
const validModes = ['custom', 'text', 'image', 'ai'];
const mode = validModes.includes(result.commentMode) ? result.commentMode : 'custom';
const radio = document.getElementById(`mode-${mode}`);
if (radio) radio.checked = true;
this.updateModePanels(mode);
await this.updateModeHint(mode);
} catch (error) {
console.error('加载评论方式失败:', error);
}
}
// 保存评论方式(写入后已打开的小红书页面会立即切换)
async saveCommentMode(mode) {
try {
await chrome.storage.local.set({ commentMode: mode });
console.log('保存评论方式:', mode);
this.updateModePanels(mode);
await this.updateModeHint(mode);
} catch (error) {
console.error('保存评论方式失败:', error);
}
}
// 根据评论方式显示/隐藏对应的配置面板
updateModePanels(mode) {
const textPanel = document.getElementById('text-comment-panel');
const imagePanel = document.getElementById('image-comment-panel');
if (textPanel) textPanel.style.display = mode === 'text' ? 'block' : 'none';
if (imagePanel) imagePanel.style.display = mode === 'image' ? 'block' : 'none';
}
// 加载自定义文字库
async loadUserComments() {
try {
const result = await chrome.storage.local.get(['userComments']);
const lines = result.userComments || [];
const textarea = document.getElementById('user-comments');
if (textarea) textarea.value = lines.join('\n');
this.updateUserCommentsCount(lines.length);
console.log('加载自定义文字库:', lines.length, '条');
} catch (error) {
console.error('加载自定义文字库失败:', error);
}
}
// 保存自定义文字库(一行一条)
async saveUserComments(showFeedback = false) {
try {
const textarea = document.getElementById('user-comments');
const lines = (textarea?.value || '')
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0);
await chrome.storage.local.set({ userComments: lines });
this.updateUserCommentsCount(lines.length);
console.log('保存自定义文字库:', lines.length, '条');
if (showFeedback) {
this.showActionStatus(true, `✓ 文字库已保存,共 ${lines.length} 条`);
}
} catch (error) {
console.error('保存自定义文字库失败:', error);
if (showFeedback) {
this.showActionStatus(false, '✗ 文字库保存失败: ' + error.message);
}
}
}
// 更新文字库条数显示
updateUserCommentsCount(count) {
const el = document.getElementById('user-comments-count');
if (el) el.textContent = count > 0 ? `共 ${count} 条` : '暂无内容';
}
// 处理选择的图片文件(压缩后保存到本地存储)
async handleImageFiles(fileList) {
const files = Array.from(fileList || []).filter((f) => f.type.startsWith('image/'));
if (files.length === 0) return;
try {
this.showActionStatus(true, `⏳ 正在处理 ${files.length} 张图片…`);
const result = await chrome.storage.local.get(['commentImages']);
const images = result.commentImages || [];
for (const file of files) {
const dataUrl = await this.compressImage(file);
images.push({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.name,
dataUrl
});
}
await chrome.storage.local.set({ commentImages: images });
this.renderImagePreviews(images);
this.showActionStatus(true, `✓ 已添加 ${files.length} 张图片,共 ${images.length} 张`);
await this.updateModeHint('image');
} catch (error) {
console.error('处理评论图片失败:', error);
this.showActionStatus(false, '✗ 图片处理失败: ' + error.message);
}
}
// 压缩图片:超过 400KB 或边长超过 1280px 时压缩为 JPEG
async compressImage(file) {
const shouldCompress = file.size > 400 * 1024;
const originalDataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
if (!shouldCompress) return originalDataUrl;
const img = await new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = originalDataUrl;
});
const maxSide = 1280;
let { width, height } = img;
if (width > maxSide || height > maxSide) {
const scale = Math.min(maxSide / width, maxSide / height);
width = Math.round(width * scale);
height = Math.round(height * scale);
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
return canvas.toDataURL('image/jpeg', 0.85);
}
// 渲染图片预览列表
async loadImagePreviews() {
try {
const result = await chrome.storage.local.get(['commentImages']);
this.renderImagePreviews(result.commentImages || []);
} catch (error) {
console.error('加载评论图片失败:', error);
}
}
renderImagePreviews(images) {
const list = document.getElementById('image-preview-list');
if (!list) return;
if (!images || images.length === 0) {
list.innerHTML = '<div class="image-preview-empty">暂无图片</div>';
return;
}
list.innerHTML = '';
images.forEach((image) => {
const item = document.createElement('div');
item.className = 'image-preview-item';
const img = document.createElement('img');
img.src = image.dataUrl;
img.alt = image.name || '';
const del = document.createElement('button');
del.type = 'button';
del.className = 'image-delete-btn';
del.textContent = '×';
del.title = '删除这张图片';
del.addEventListener('click', () => this.deleteCommentImage(image.id));
item.appendChild(img);
item.appendChild(del);
list.appendChild(item);
});
}
// 删除一张评论图片
async deleteCommentImage(id) {
try {
const result = await chrome.storage.local.get(['commentImages']);
const images = (result.commentImages || []).filter((img) => img.id !== id);
await chrome.storage.local.set({ commentImages: images });
this.renderImagePreviews(images);
this.showActionStatus(true, `✓ 已删除,剩余 ${images.length} 张`);
await this.updateModeHint('image');
} catch (error) {
console.error('删除评论图片失败:', error);
}
}
// ===== 授权系统(CardAuth) =====
// 功能门槛:有效授权 或 试用期内可用;试用结束锁定
async isFeatureAllowed() {
try {
const status = await chrome.runtime.sendMessage({ action: 'authStatus' });
const state = status && status.status ? status.status.state : 'locked';
if (state === 'locked') {
this.showActionStatus(false, '✗ 试用已结束,请购买授权或输入卡密激活');
return false;
}
return true;
} catch (error) {
// 后台不可达时不阻断功能
console.error('授权状态检查失败:', error);
return true;
}
}
// 初始化授权区:状态 / 公告
async initAuth() {
await this.renderAuthStatus();
await this.loadAnnouncements();
// 后台复验或购买激活后自动刷新
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace !== 'local') return;
if (changes.licenseState) this.renderAuthStatus();
if (changes.announcements) this.renderAnnouncements((changes.announcements.newValue || {}).list || []);
});
}
// 渲染授权状态徽章
async renderAuthStatus() {
const badge = document.getElementById('auth-badge');
const detail = document.getElementById('auth-detail');
if (!badge || !detail) return;
try {
const status = await chrome.runtime.sendMessage({ action: 'authStatus' });
const s = status && status.status ? status.status : { state: 'locked' };
badge.classList.remove('authorized', 'trial', 'locked');
this.stopTrialCountdown();
if (s.state === 'authorized') {
badge.classList.add('authorized');
badge.textContent = '已授权';
const license = s.license || {};
if (license.isPermanent) {
detail.textContent = '永久授权 · 卡密 ' + this.maskCardKey(license.cardKey);
} else if (license.expireTime) {
const days = license.remainingDays != null ? ` · 剩余 ${license.remainingDays} 天` : '';
detail.textContent = `到期 ${license.expireTime}${days}`;
} else {
detail.textContent = '授权有效';
}
} else if (s.state === 'trial') {
badge.classList.add('trial');
badge.textContent = '试用中';
this.startTrialCountdown(s.trialEndAt || (Date.now() + 3600000));
} else {
badge.classList.add('locked');
badge.textContent = '未授权';
detail.textContent = '试用已结束:请购买授权或输入卡密激活后使用';
}
} catch (error) {
console.error('获取授权状态失败:', error);
badge.classList.add('locked');
badge.textContent = '未知';
detail.textContent = '授权状态获取失败,请重新打开侧边栏';
}
}
// 试用倒计时(精确到秒,每秒刷新)
startTrialCountdown(endAt) {
this.stopTrialCountdown();
const tick = () => {
const remain = endAt - Date.now();
const detail = document.getElementById('auth-detail');
if (remain <= 0) {
this.stopTrialCountdown();
this.renderAuthStatus(); // 到期后刷新为锁定状态
return;
}
if (detail) {
detail.textContent = `免费试用剩余 ${this.formatRemain(remain)} · 试用结束后需购买授权或输入卡密激活`;
}
};
tick();
this.trialTimer = setInterval(tick, 1000);
}
stopTrialCountdown() {
if (this.trialTimer) {
clearInterval(this.trialTimer);
this.trialTimer = null;
}
}
// 毫秒 → HH:MM:SS
formatRemain(ms) {
const totalSec = Math.max(0, Math.floor(ms / 1000));
const pad = (n) => String(n).padStart(2, '0');
return `${pad(Math.floor(totalSec / 3600))}:${pad(Math.floor((totalSec % 3600) / 60))}:${pad(totalSec % 60)}`;
}
maskCardKey(key) {
if (!key) return '';
const k = String(key);
return k.length > 10 ? k.slice(0, 8) + '****' + k.slice(-4) : k;
}
// 手动输入卡密激活
async activateCard() {
const input = document.getElementById('card-key-input');
const btn = document.getElementById('activate-btn');
const cardKey = (input?.value || '').trim();
if (!cardKey) {
this.showOrderStatus(false, '请输入卡密');
return;
}
if (btn) { btn.disabled = true; btn.textContent = '激活中…'; }
this.showOrderStatus(true, '⏳ 正在验证卡密…');
try {
const res = await chrome.runtime.sendMessage({ action: 'authActivate', cardKey });
if (res && res.ok && res.licenseState && res.licenseState.valid) {
input.value = '';
this.showOrderStatus(true, `✓ 激活成功!${res.licenseState.isPermanent ? '永久授权' : '到期 ' + res.licenseState.expireTime}`);
await this.renderAuthStatus();
} else {
const msg = (res && res.licenseState && res.licenseState.message) || (res && res.message) || '激活失败';
this.showOrderStatus(false, '✗ ' + msg);
}
} catch (error) {
this.showOrderStatus(false, '✗ 激活失败: ' + error.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = '激活'; }
}
}
// 购买流程:下单(带机器码)→ 打开支付页 → 轮询订单 → 自动发卡激活
async startBuyFlow(payType) {
const payRow = document.getElementById('pay-select-row');
if (payRow) payRow.hidden = true;
this.showOrderStatus(true, '⏳ 正在创建订单…');
try {
const res = await chrome.runtime.sendMessage({ action: 'authCreateOrder', payType });
if (!res || !res.ok) {
this.showOrderStatus(false, '✗ 创建订单失败: ' + ((res && res.message) || '未知错误'));
return;
}
const order = res.order;
this.showOrderStatus(true, `订单 ${order.orderNo}(¥${order.amount})已创建,请在新页面完成支付,支付成功后自动激活…`);
if (res.payUrl) {
chrome.tabs.create({ url: res.payUrl });
}
this.startOrderPolling(order.orderNo);
} catch (error) {
this.showOrderStatus(false, '✗ 购买失败: ' + error.message);
}
}
// 轮询订单:支付+自动发卡后自动激活
startOrderPolling(orderNo) {
if (this.orderPollTimer) {
clearInterval(this.orderPollTimer);
this.orderPollTimer = null;
}
let attempts = 0;
const maxAttempts = 120; // 5秒 × 120 = 最多10分钟
this.orderPollTimer = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(this.orderPollTimer);
this.orderPollTimer = null;
this.showOrderStatus(false, `✗ 等待支付超时。订单号 ${orderNo},支付后可凭订单号到购买中心查询,或用收到的卡密在此激活`);
return;
}
try {
const res = await chrome.runtime.sendMessage({ action: 'authQueryOrder', orderNo });
if (!res || !res.ok) return;
if (res.activated) {
clearInterval(this.orderPollTimer);
this.orderPollTimer = null;
this.showOrderStatus(true, `✓ 支付成功,已自动发卡并激活生效!${res.expireTime ? '到期 ' + res.expireTime : ''}`);
await this.renderAuthStatus();
const input = document.getElementById('card-key-input');
if (input && res.cardKey) input.value = res.cardKey;
} else if (res.paid && res.cardKey && !res.activated) {
clearInterval(this.orderPollTimer);
this.orderPollTimer = null;
this.showOrderStatus(false, '✗ 已支付但激活失败: ' + ((res.licenseState && res.licenseState.message) || '请手动输入卡密激活'));
} else if (attempts % 6 === 0) {
this.showOrderStatus(true, `⏳ 等待支付中…(订单 ${orderNo},已等待 ${attempts * 5} 秒)`);
}
} catch (error) {
console.error('订单轮询失败:', error);
}
}, 5000);
}
showOrderStatus(ok, text) {
const el = document.getElementById('order-status');
if (!el) return;
el.textContent = text;
el.className = 'order-status ' + (ok ? 'ok' : 'fail');
}
// 拉取并渲染公告
async loadAnnouncements() {
try {
const res = await chrome.runtime.sendMessage({ action: 'authGetAnnouncements' });
const ann = res && res.announcements ? res.announcements : null;
const cached = ann || (await chrome.storage.local.get(['announcements'])).announcements;
if (cached) {
this.renderAnnouncements(cached.list || []);
await this.maybeShowPopupAnnouncements(cached.popup || []);
}
} catch (error) {
console.error('加载公告失败:', error);
}
}
renderAnnouncements(list) {
const box = document.getElementById('announcement-box');
const container = document.getElementById('announcement-list');
if (!box || !container) return;
if (!list || list.length === 0) {
box.hidden = true;
return;
}
box.hidden = false;
container.innerHTML = '';
list.slice(0, 10).forEach((item) => {
const div = document.createElement('div');
div.className = 'announcement-item';
if (item.created_at) {
const time = document.createElement('span');
time.className = 'announcement-time';
time.textContent = String(item.created_at).slice(0, 16);
div.appendChild(time);
}
// 后台字段:title + content
if (item.title) {
const strong = document.createElement('b');
strong.textContent = item.title;
div.appendChild(strong);
div.appendChild(document.createElement('br'));
}
div.appendChild(document.createTextNode(item.content || ''));
container.appendChild(div);
});
}
// 弹窗公告:每条只弹一次
async maybeShowPopupAnnouncements(popupList) {
if (!popupList || popupList.length === 0) return;
const result = await chrome.storage.local.get(['seenPopupAnnouncements']);
const seen = result.seenPopupAnnouncements || [];
const unseen = popupList.filter((item) => !seen.includes(String(item.id)));
if (unseen.length === 0) return;
const texts = unseen.map((item) => {
const title = item.title || '';
const content = item.content || '';
return [title, content].filter(Boolean).join('\n');
}).filter(Boolean);
if (texts.length > 0) {
alert('📢 公告\n\n' + texts.join('\n\n—\n\n'));
}
await chrome.storage.local.set({
seenPopupAnnouncements: [...seen, ...unseen.map((item) => String(item.id))]
});
}
// ===== 评论记录 =====
// 从存储加载评论记录
async loadCommentLogs() {
try {
const result = await chrome.storage.local.get(['commentLogs']);
this.renderCommentLogs(result.commentLogs || []);
} catch (error) {
console.error('加载评论记录失败:', error);
}
}
// 计算今日/本月/总计统计
computeLogStats(logs) {
const now = new Date();
const todayKey = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
const monthKey = `${now.getFullYear()}-${now.getMonth()}`;
let today = 0;
let month = 0;
logs.forEach((record) => {
const d = new Date(record.timestamp);
if (`${d.getFullYear()}-${d.getMonth()}-${d.getDate()}` === todayKey) today++;
if (`${d.getFullYear()}-${d.getMonth()}` === monthKey) month++;
});
return { today, month, total: logs.length };
}
// 渲染统计 + 列表
renderCommentLogs(logs) {
const stats = this.computeLogStats(logs);
const setStat = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
setStat('stat-today', stats.today);
setStat('stat-month', stats.month);
setStat('stat-total', stats.total);
const list = document.getElementById('log-list');
if (!list) return;
if (!logs || logs.length === 0) {
list.innerHTML = '<div class="log-empty">暂无评论记录,成功评论后会自动出现在这里</div>';
return;
}
const modeLabels = { custom: '固定池', text: '文字', image: '图片', ai: 'AI' };
list.innerHTML = '';
logs.slice(0, 100).forEach((record) => {
const item = document.createElement('div');
item.className = 'log-item' + (record.url ? '' : ' no-url');
item.title = record.url ? '点击打开笔记' : '无笔记链接';
if (record.thumb) {
const thumb = document.createElement('img');
thumb.className = 'log-thumb';
thumb.src = record.thumb;
item.appendChild(thumb);
}
const body = document.createElement('div');
body.className = 'log-item-body';
const title = document.createElement('div');
title.className = 'log-item-title';
title.textContent = record.title || '无标题笔记';
const comment = document.createElement('div');
comment.className = 'log-item-comment';
comment.textContent = record.comment || '';
const meta = document.createElement('div');
meta.className = 'log-item-meta';
const time = new Date(record.timestamp);
const pad = (n) => String(n).padStart(2, '0');
const tag = document.createElement('span');
tag.className = 'log-mode-tag';
tag.textContent = modeLabels[record.mode] || record.mode || '';
meta.appendChild(tag);
meta.appendChild(document.createTextNode(
`${time.getFullYear()}-${pad(time.getMonth() + 1)}-${pad(time.getDate())} ${pad(time.getHours())}:${pad(time.getMinutes())}`
));
body.appendChild(title);
body.appendChild(comment);
body.appendChild(meta);
item.appendChild(body);
if (record.url) {
const openIcon = document.createElement('span');
openIcon.className = 'log-open-icon';
openIcon.textContent = '↗';
item.appendChild(openIcon);
item.addEventListener('click', () => {
chrome.tabs.create({ url: record.url });
});
}
list.appendChild(item);
});
}
// 提示当前配置是否真正生效
async updateModeHint(mode) {
const hint = document.getElementById('mode-hint');
if (!hint) return;
const aiRadio = document.getElementById('mode-ai');
const current = mode || (aiRadio && aiRadio.checked ? 'ai' : 'custom');
const keyInput = document.getElementById('api-key');
const hasKey = !!(keyInput && keyInput.value && keyInput.value.trim());
let storageInfo = {};
try {
storageInfo = await chrome.storage.local.get(['userComments', 'commentImages']);
} catch (error) {
console.error('读取提示信息失败:', error);
}
const textCount = (storageInfo.userComments || []).length;
const imageCount = (storageInfo.commentImages || []).length;
hint.classList.remove('warn');
if (current === 'ai' && !hasKey) {
hint.textContent = '⚠ AI 模式需要先填 API 密钥,否则会退回固定评论';
hint.classList.add('warn');
} else if (current === 'text' && textCount === 0) {
hint.textContent = '⚠ 自定义文字模式还没有文字内容,请在下方填写并保存';
hint.classList.add('warn');
} else if (current === 'image' && imageCount === 0) {
hint.textContent = '⚠ 图片回复模式还没有上传图片,请在下方选择图片';
hint.classList.add('warn');
} else if (current === 'custom' && hasKey) {
hint.textContent = '⚠ 当前是固定评论池,下方模型配置不会生效;要按笔记生成请点「AI 按笔记生成」';
hint.classList.add('warn');
} else if (current === 'ai') {
hint.textContent = '✓ 每篇笔记都会调用下方模型,按笔记内容生成评论';
} else if (current === 'text') {
hint.textContent = `✓ 每篇笔记发送一条自定义文字(共 ${textCount} 条循环)`;
} else if (current === 'image') {
hint.textContent = `✓ 每篇笔记发送一张图片(共 ${imageCount} 张循环)`;
} else {
hint.textContent = '固定评论池 = 循环发预设短评;下方模型配置暂不生效';
}
}
// 加载自定义模型配置(API地址 + 模型名)
async loadModelConfig() {
try {
const result = await chrome.storage.local.get(['apiBaseUrl', 'modelName']);
const baseUrl = (result.apiBaseUrl || '').trim() || DEFAULT_AI.apiBaseUrl;
const modelName = (result.modelName || '').trim() || DEFAULT_AI.model;
const baseUrlInput = document.getElementById('api-base-url');
const modelInput = document.getElementById('model-name');
if (baseUrlInput) baseUrlInput.value = baseUrl;
if (modelInput) modelInput.value = modelName;
// 首次使用时写入默认值
if (!result.apiBaseUrl || !result.modelName) {
await chrome.storage.local.set({ apiBaseUrl: baseUrl, modelName: modelName });
}
console.log('加载模型配置:', baseUrl, modelName);
} catch (error) {
console.error('加载模型配置失败:', error);
}
}
// 保存自定义模型配置
async saveModelConfig(changes) {
try {
await chrome.storage.local.set(changes);
console.log('保存模型配置:', Object.keys(changes).join(', '));
} catch (error) {
console.error('保存模型配置失败:', error);
}
}
// 加载AI提示词
async loadAiPrompt() {
try {
const result = await chrome.storage.local.get(['aiPrompt']);
const defaultPrompt = '根据笔记内容来生成符合笔记的评论。最后只需要输出评论内容,不要输出你的推理,不要输出任何与评论无关的内容,不要输出#相关的内容,不要输出笔记关键词相关的内容。评论不要超过300字。这个是小红书的笔记内容:\n\n{{content}}\n\n';
const prompt = result.aiPrompt || defaultPrompt;
const textarea = document.getElementById('ai-prompt');
if (textarea) textarea.value = prompt;
console.log('加载AI提示词');
} catch (error) {
console.error('加载AI提示词失败:', error);
}
}
// 保存AI提示词
async saveAiPrompt() {
try {
const textarea = document.getElementById('ai-prompt');
const prompt = (textarea?.value || '').trim();
await chrome.storage.local.set({ aiPrompt: prompt });
console.log('保存AI提示词');
alert('提示词已保存');
} catch (error) {
console.error('保存AI提示词失败:', error);
alert('保存提示词失败: ' + error.message);
}
}
// 恢复默认AI提示词
async resetAiPromptToDefault() {
try {
const defaultPrompt = '根据笔记内容来生成符合笔记的评论。最后只需要输出评论内容,不要输出你的推理,不要输出任何与评论无关的内容,不要输出#相关的内容,不要输出笔记关键词相关的内容。评论不要超过300字。这个是小红书的笔记内容:\n\n{{content}}\n\n';
await chrome.storage.local.set({ aiPrompt: defaultPrompt });
const textarea = document.getElementById('ai-prompt');
if (textarea) textarea.value = defaultPrompt;
console.log('恢复默认AI提示词');
alert('已恢复默认提示词');
} catch (error) {
console.error('恢复默认AI提示词失败:', error);
alert('恢复默认提示词失败: ' + error.message);
}
}
// 更新AI状态显示
updateAiStatus() {
const apiKey = document.getElementById('api-key').value;
const isEnabled = apiKey && apiKey.length > 0;
const modelName = (document.getElementById('model-name')?.value || '').trim() || DEFAULT_AI.model;
const baseUrl = (document.getElementById('api-base-url')?.value || '').trim();
const statusIndicator = document.querySelector('.status-indicator');
const statusText = document.querySelector('.status-text');
const testBtn = document.getElementById('test-ai-btn');
if (isEnabled) {
statusIndicator.textContent = '🟢';
statusText.textContent = `已启用 · ${modelName} · ${baseUrl}`;
testBtn.disabled = false;
} else {
statusIndicator.textContent = '🔴';
statusText.textContent = '未配置API密钥';
testBtn.disabled = true;
}
this.updateModeHint();
}
// 测试AI生成功能(走后台调用自定义模型)
async testAiGeneration() {
const apiKey = document.getElementById('api-key').value;
const baseUrl = document.getElementById('api-base-url')?.value;
const modelName = document.getElementById('model-name')?.value;
const resultEl = document.getElementById('test-ai-result');
const testBtn = document.getElementById('test-ai-btn');
const showResult = (ok, text) => {
if (!resultEl) {
alert(text);
return;
}
resultEl.textContent = text;
resultEl.className = 'test-ai-result ' + (ok ? 'ok' : 'fail');
};
if (!apiKey || apiKey.length === 0) {
showResult(false, '✗ 请先配置API密钥');
return;
}
if (testBtn) {
testBtn.disabled = true;
testBtn.textContent = '测试中…';
}
showResult(true, '⏳ 请求中…');
try {
const apiResult = await chrome.runtime.sendMessage({
action: 'callAiApi',
apiKey: apiKey,
baseUrl: baseUrl,
model: modelName,
prompt: '你好,请简单回复一下',
maxTokens: 300,
timeoutMs: 60000
});
if (apiResult && apiResult.ok) {
const reply = String(apiResult.content).slice(0, 40);
showResult(true, `✓ 成功 · ${apiResult.model} · ${reply}`);
} else {
showResult(false, `✗ 失败 · ${(apiResult && apiResult.error) || '后台无响应,请重新加载插件'}`);
}
} catch (error) {
console.error('AI测试失败:', error);
showResult(false, '✗ 失败 · ' + error.message);
} finally {
if (testBtn) {
testBtn.disabled = false;
testBtn.textContent = '测试AI';
}
this.updateAiStatus();
}
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
new AutoCommentManager();
});