From 8193e6c0c5a4f4f670b108e42f91208e586ba571 Mon Sep 17 00:00:00 2001
From: 88531 <88531@users.noreply.github.com>
Date: Sat, 10 Oct 2026 14:02:29 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=8F=E7=BA=A2=E4=B9=A6=E8=87=AA?=
=?UTF-8?q?=E5=8A=A8=E8=AF=84=E8=AE=BA=E5=8A=A9=E6=89=8B=20v1.7.5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 侧边栏常驻:搜索/自动点击/自动评论全流程
- 4种评论方式:固定池/自定义文字/图片回复/AI按笔记生成
- 按地区搜索、评论记录统计(今日/本月,点击直达笔记)
- CardAuth 授权:1天试用倒计时、卡密激活、购买自动发卡
- 公告推送、安装统计、小红书风格图标
---
.gitignore | 6 +
README.md | 163 +++
background.js | 659 ++++++++++++
content.css | 423 ++++++++
content.js | 2529 +++++++++++++++++++++++++++++++++++++++++++++
icons/icon128.png | Bin 0 -> 2046 bytes
icons/icon128.svg | 15 +
icons/icon16.png | Bin 0 -> 376 bytes
icons/icon16.svg | 4 +
icons/icon32.png | Bin 0 -> 619 bytes
icons/icon32.svg | 9 +
icons/icon48.png | Bin 0 -> 819 bytes
icons/icon48.svg | 9 +
manifest.json | 42 +
sidepanel.css | 1042 +++++++++++++++++++
sidepanel.html | 219 ++++
sidepanel.js | 1285 +++++++++++++++++++++++
17 files changed, 6405 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 background.js
create mode 100644 content.css
create mode 100644 content.js
create mode 100644 icons/icon128.png
create mode 100644 icons/icon128.svg
create mode 100644 icons/icon16.png
create mode 100644 icons/icon16.svg
create mode 100644 icons/icon32.png
create mode 100644 icons/icon32.svg
create mode 100644 icons/icon48.png
create mode 100644 icons/icon48.svg
create mode 100644 manifest.json
create mode 100644 sidepanel.css
create mode 100644 sidepanel.html
create mode 100644 sidepanel.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e329705
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+Thumbs.db
+desktop.ini
+.DS_Store
+*.log
+.vscode/
+.idea/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..aaad7dd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,163 @@
+# 📕 小红书自动评论助手
+
+> 一键搜索 · 自动点击 · 智能评论 —— 小红书运营引流的效率神器
+
+[](https://dl.88531.cn/xhs.html)
+[](#安装方法)
+[](#开发说明)
+
+一个专注于自动评论功能的浏览器插件:在浏览器**右侧侧边栏常驻**,输入关键词即可自动搜索小红书、逐篇点开笔记、发表评论,全程无需人工干预。
+
+---
+
+## 🌟 为什么选择我们
+
+| 亮点 | 说明 |
+|---|---|
+| 🚀 **全流程自动化** | 输入关键词 → 自动开搜 → 自动点开每篇笔记 → 自动写评论 → 自动提交,一条龙跑完 |
+| 🤖 **4 种评论方式** | 固定评论池 / 自定义文字库 / **图片回复** / **AI 按笔记生成**,随心切换 |
+| 🧠 **AI 智能评论** | 接入任意 OpenAI 兼容接口,每篇笔记结合内容生成不同评论,告别复读机 |
+| 🖼️ **图片评论** | 上传表情包/图片,每篇笔记自动发一张,多张循环,互动更自然 |
+| 📍 **按地区搜索** | 选「上海」「杭州西湖区」自动拼进关键词,精准触达同城流量 |
+| 📊 **评论数据统计** | 今日/本月/总计成功数一目了然,每条记录带标题+评论内容,**点击直达笔记** |
+| 🔐 **免费试用 1 天** | 装完即用,满意再买;插件内支付宝/微信购买,**支付成功自动发卡自动激活** |
+| 📢 **公告推送** | 官方公告实时下发(弹窗/列表),运营消息不错过 |
+| 🛡️ **稳定可靠** | 元素检测+备用方案、失败自动重试、内存自动防护、页面结构变化可快速适配 |
+| 🔒 **数据本地存储** | 评论库、图片、记录全部存在你自己的浏览器里,不上传第三方 |
+
+## 📦 功能总览
+
+- **🔍 智能搜索**:关键词搜索 + 地区筛选(18 个主要城市 + 自定义到区/商圈级)
+- **🤖 自动点击**:精确定位笔记卡片链接,支持「展开内容」或「简单点击」两种模式,自动滚动加载更多
+- **💬 评论方式**:
+ - 固定评论池:循环发预设短评(👍 赞!/💯 太棒了!…)
+ - 自定义文字:侧边栏文字库一行一条,循环发送
+ - 图片回复:上传图片自动压缩存本地,预览/删除管理
+ - AI 按笔记生成:提示词可自定义(`{{content}}` 占位符),失败自动降级
+- **📊 评论记录**:成功评论自动记录,今日/本月/总计统计,点击记录打开对应笔记
+- **🔐 授权体系**:1 天试用(倒计时精确到秒)、卡密激活、插件内购买自动发卡
+- **🧪 元素检测**:一键测试页面元素识别情况,方便小红书改版后快速排查
+
+## 🚀 安装方法
+
+1. 下载插件文件到本地(或 `git clone` 本仓库)
+2. 打开 Chrome / Edge,进入扩展程序页面(`chrome://extensions/` 或 `edge://extensions/`)
+3. 开启「开发者模式」
+4. 点击「加载已解压的扩展程序」,选择插件文件夹
+5. 工具栏出现红色「小红书」图标,点击即在右侧打开侧边栏
+
+## 📖 使用方法
+
+### 打开侧边栏
+- 点击工具栏图标,界面**固定在浏览器右侧**,切换标签页不会关闭,方便边看边操作
+- 再点一次图标可收起
+
+### 授权与购买
+1. 安装后自动进入 **1 天免费试用**(侧边栏顶部显示精确到秒的倒计时)
+2. 试用结束后需激活:输入卡密点「激活」,或点「购买授权」
+3. 购买流程:点购买 → 选支付宝/微信 → 新页面完成支付 → **支付成功自动发卡并激活生效**,无需手动输卡密
+4. 已购用户也可在 [购买中心](https://auth.88531.cn/shop) 用订单号查询卡密,回来手动激活
+5. 授权绑定本机浏览器,换设备请联系管理员重置
+
+### 自动搜索评论
+1. 输入搜索关键词
+2. (可选)选择地区:如「北京」「自定义… 杭州西湖区」
+3. 点击「🔍 搜索」→ 自动打开搜索结果页并开始评论流程,状态实时显示在面板内
+
+> 说明:小红书网页版没有原生地区筛选,地区以关键词形式参与搜索(实际搜索词为「关键词 地区」)。
+
+### 评论方式(四选一,改完立即生效)
+- **固定评论池**:循环发预设短评,不调用模型
+- **自定义文字**:文字库一行一条,循环发送
+- **图片回复**:每篇笔记发一张图片,多张循环
+- **AI 按笔记生成**:每篇笔记调用模型按内容生成
+
+未配置对应内容时(如选了文字模式但库是空的),选项下方会给出橙色提示。
+
+### 手动触发 / 元素检测
+- 在小红书页面点「🤖 自动点击笔记」立即开始;点「🧪 测试元素检测」排查页面识别问题
+
+### 配置 AI 评论(自定义模型)
+1. 侧边栏「AI评论生成」处填写(改动即时保存):
+ - **API密钥**:服务商的 key(`sk-...`),没有?[👉 免费获取模型 KEY](https://ai.88531.cn)
+ - **API地址**:任意 OpenAI 兼容接口(未带 `/v1` 自动补全)
+ - **模型名**:如 `mimo-v2.6-flash`、`deepseek-ai/DeepSeek-R1`
+2. 点「测试AI」验证连通,状态显示 🟢 已启用即可用
+
+## 🔧 自动点击调试
+
+1. **控制台日志**:F12 → Console 查看完整操作日志
+2. **元素检测**:侧边栏点「🧪 测试元素检测」查看识别详情
+3. **常见问题**:
+ - 页面未完全加载 → 等加载完成再启动
+ - 小红书改版 → 用元素检测确认选择器,更新 `content.js` 中的 CSS 选择器
+ - 评论没发出去 → 检查网络/登录状态,观察控制台报错
+
+## ⚠️ 注意事项
+
+- 需要在小红书官网 (xiaohongshu.com) 登录状态下使用
+- 评论内容请遵守平台规范,建议合理控制频率,避免账号风控
+- 页面结构变化可能导致兼容性问题,欢迎提 Issue 反馈
+
+## 📜 版本历史
+
+当前版本:**1.7.5**
+
+
+展开查看完整变更记录(v1.2.0 ~ v1.7.5)
+
+### 1.7.5
+- 试用倒计时精确到秒(HH:MM:SS 每秒刷新,归零自动锁定)
+
+### 1.7.4
+- 移除「发现新版本」提示条;底部新增「⬇️ 获取最新版本」大按钮直达官方下载页
+
+### 1.7.3
+- 修复更新提示残留;公告按「标题+内容+时间」展示
+
+### 1.7.2
+- 「AI评论生成」上方新增橙色发光横幅「免费获取模型KEY → ai.88531.cn」
+
+### 1.7.1
+- 图标改为小红书风格 PNG(品牌红底 + 白色「小红书」字样),工具栏一眼可认
+
+### 1.7.0
+- 接入 CardAuth 授权系统:1 天试用、卡密激活、插件内购买**自动发卡自动激活**
+- 公告推送(弹窗每条一次 + 列表)、安装统计、30 分钟心跳
+
+### 1.6.0
+- 新增「自定义文字」「图片回复」两种评论方式(评论方式扩展为四选一)
+
+### 1.5.0
+- 新增「按地区搜索」(地区下拉 + 自定义,自动拼进关键词)
+
+### 1.4.x
+- 侧边栏常驻、评论方式开关、图标识别小字
+
+### 1.3.x
+- 侧边栏(Side Panel)改版;修复思考型模型 AI 正文为空的问题
+
+### 1.2.0
+- AI 模型自定义配置(API地址/模型名/密钥),请求改由后台发出规避跨域
+
+
+
+## 🛠 开发说明
+
+纯 JavaScript 开发,**无任何外部依赖**,代码结构清晰,便于二次开发:
+
+```
+manifest.json # 扩展清单(MV3)
+background.js # 后台服务:AI请求代理、授权/公告/心跳
+content.js # 内容脚本:笔记点击、评论输入与提交、记录
+sidepanel.html/js/css# 侧边栏 UI(搜索/评论配置/授权/记录)
+icons/ # 16/32/48/128 小红书风格图标
+```
+
+## 📄 免责声明
+
+本插件仅供学习与效率研究使用。使用本插件产生的一切行为(评论内容、频率等)由使用者自行负责,请遵守小红书平台规范及相关法律法规。
+
+---
+
+**⬇️ [获取最新版本](https://dl.88531.cn/xhs.html)** · 反馈与合作请提 Issue
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..ff65b63
--- /dev/null
+++ b/background.js
@@ -0,0 +1,659 @@
+// 后台脚本
+// 点击工具栏图标 → 直接在浏览器右侧打开/收起侧边栏(而不是弹窗)
+if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
+ chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((error) => {
+ console.log('设置侧边栏行为失败:', error);
+ });
+}
+
+// 默认自定义模型配置(OpenAI 兼容接口)
+const DEFAULT_AI = {
+ apiBaseUrl: 'https://ai.88531.cn/v1',
+ model: 'mimo-v2.6-flash'
+};
+
+// ===== CardAuth 授权系统配置 =====
+const AUTH_BASE = 'https://auth.88531.cn';
+const AUTH_PROJECT_ID = 3;
+const AUTH_API_KEY = 'ak_bbf83b76df8219388d7f60b9e67c7db2';
+const CLIENT_VERSION = '1.7.5';
+const TRIAL_MS = 24 * 60 * 60 * 1000; // 未授权试用1天
+
+// 把用户填写的地址规范化为完整的 chat/completions 端点
+function buildChatCompletionsUrl(baseUrl) {
+ let base = String(baseUrl || '').trim() || DEFAULT_AI.apiBaseUrl;
+ base = base.replace(/\/+$/, '');
+ if (/\/chat\/completions$/i.test(base)) {
+ return base;
+ }
+ if (!/\/v\d+$/i.test(base)) {
+ base += '/v1'; // 未带版本号时自动补 /v1(如 https://api.deepseek.com)
+ }
+ return base + '/chat/completions';
+}
+
+// 构造请求体
+function buildChatBody(model, prompt, maxTokens) {
+ return {
+ model: model,
+ messages: [{ role: 'user', content: prompt }],
+ temperature: 0.7,
+ max_tokens: maxTokens,
+ presence_penalty: 0,
+ frequency_penalty: 0,
+ // 思考型模型(mimo / DeepSeek-R1 等)会先写 reasoning_content,
+ // 不关思考时正文容易被 max_tokens 截断 → 返回空。
+ // 支持的端点直接关掉思考;不支持的端点会报 400,下面自动去掉该参数重试
+ thinking: { type: 'disabled' }
+ };
+}
+
+// 从各种 OpenAI 兼容返回结构里取正文(兼容 content 字符串 / 分段数组 / text / output_text 等)
+function extractChatContent(data) {
+ if (!data || typeof data !== 'object') return '';
+ const pick = (value) => {
+ if (typeof value === 'string') return value.trim();
+ if (Array.isArray(value)) {
+ return value.map(part => {
+ if (typeof part === 'string') return part;
+ if (part && typeof part === 'object') return part.text || part.content || '';
+ return '';
+ }).join('').trim();
+ }
+ return '';
+ };
+ try {
+ const choice = Array.isArray(data.choices) ? data.choices[0] : null;
+ if (choice) {
+ const fromMessage = choice.message ? pick(choice.message.content) : '';
+ if (fromMessage) return fromMessage;
+ const fromText = pick(choice.text);
+ if (fromText) return fromText;
+ }
+ return pick(data.output_text) || pick(data.output);
+ } catch (error) {
+ return '';
+ }
+}
+
+chrome.runtime.onInstalled.addListener(() => {
+ console.log('小红书自动评论助手已安装');
+
+ // 初始化存储
+ chrome.storage.local.get(['expandContent', 'commentText', 'apiKey', 'aiPrompt', 'apiBaseUrl', 'modelName'], (result) => {
+ if (result.expandContent === undefined) {
+ chrome.storage.local.set({ expandContent: true });
+ }
+ if (!result.commentText) {
+ chrome.storage.local.set({ commentText: '👍 赞!' });
+ }
+ if (!result.apiKey) {
+ chrome.storage.local.set({ apiKey: '' });
+ }
+ // 自定义模型配置:默认使用 ai.88531.cn/v1 的 mimo-v2.6-flash
+ if (!result.apiBaseUrl) {
+ chrome.storage.local.set({ apiBaseUrl: DEFAULT_AI.apiBaseUrl });
+ }
+ if (!result.modelName) {
+ chrome.storage.local.set({ modelName: DEFAULT_AI.model });
+ }
+ if (!result.aiPrompt) {
+ const defaultPrompt = '根据笔记内容来生成符合笔记的评论。最后只需要输出评论内容,不要输出你的推理,不要输出任何与评论无关的内容,不要输出#相关的内容,不要输出笔记关键词相关的内容。评论不要超过300字。这个是小红书的笔记内容:\n\n{{content}}\n\n';
+ chrome.storage.local.set({ aiPrompt: defaultPrompt });
+ }
+ });
+
+ // 授权系统:登记安装(计入后台安装统计)+ 初始化试用期 + 拉公告
+ authOnInstalled();
+});
+
+// 浏览器启动 / 服务工作线程重启时恢复定时任务
+chrome.runtime.onStartup?.addListener?.(() => {
+ setupAuthAlarms();
+});
+
+// ===== CardAuth 授权系统 =====
+
+// 统一请求封装(CardAuth 统一响应: {code, message, data})
+async function authFetch(path, { method = 'GET', body = null, needKey = false } = {}) {
+ const headers = { 'Content-Type': 'application/json' };
+ if (needKey) headers['X-Api-Key'] = AUTH_API_KEY;
+
+ const response = await fetch(AUTH_BASE + path, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ });
+
+ let json = {};
+ try {
+ json = await response.json();
+ } catch (error) {
+ json = {};
+ }
+ return {
+ ok: response.ok && json.code === 200,
+ status: response.status,
+ code: json.code,
+ message: json.message || `HTTP ${response.status}`,
+ data: json.data
+ };
+}
+
+// 获取/生成设备指纹(授权绑定与安装统计的唯一标识)
+async function getDeviceId() {
+ const result = await chrome.storage.local.get(['deviceId']);
+ if (result.deviceId) return result.deviceId;
+ const deviceId = 'EXT-' + crypto.randomUUID();
+ await chrome.storage.local.set({ deviceId });
+ console.log('已生成设备指纹:', deviceId);
+ return deviceId;
+}
+
+// 安装初始化:试用期起点 + 设备指纹 + 安装上报 + 公告
+async function authOnInstalled() {
+ try {
+ const result = await chrome.storage.local.get(['trialStartedAt']);
+ if (!result.trialStartedAt) {
+ await chrome.storage.local.set({ trialStartedAt: Date.now() });
+ console.log('试用期开始(1天)');
+ }
+ } catch (error) {
+ console.error('初始化试用期失败:', error);
+ }
+
+ setupAuthAlarms();
+
+ try {
+ await authCheckUpdate(); // 后台计入安装统计 + 检查更新
+ } catch (error) {
+ console.error('安装上报失败:', error);
+ }
+
+ try {
+ await authGetAnnouncements();
+ } catch (error) {
+ console.error('拉取公告失败:', error);
+ }
+
+ // 已有卡密则静默复验
+ try {
+ const { licenseState } = await chrome.storage.local.get(['licenseState']);
+ if (licenseState && licenseState.cardKey && licenseState.valid) {
+ await authVerify(licenseState.cardKey);
+ }
+ } catch (error) {
+ console.error('启动时授权复验失败:', error);
+ }
+}
+
+// 注册定时任务:心跳/复验/公告/更新检查
+function setupAuthAlarms() {
+ if (!chrome.alarms) return;
+ chrome.alarms.create('authHeartbeat', { periodInMinutes: 30 });
+ chrome.alarms.create('authVerify', { periodInMinutes: 10 });
+ chrome.alarms.create('authAnnounce', { periodInMinutes: 60 });
+ chrome.alarms.create('authUpdateCheck', { periodInMinutes: 60 });
+}
+
+chrome.alarms?.onAlarm?.addListener(async (alarm) => {
+ try {
+ if (alarm.name === 'authHeartbeat') {
+ await authHeartbeat();
+ } else if (alarm.name === 'authVerify') {
+ const { licenseState } = await chrome.storage.local.get(['licenseState']);
+ if (licenseState && licenseState.cardKey && licenseState.valid) {
+ await authVerify(licenseState.cardKey);
+ }
+ } else if (alarm.name === 'authAnnounce') {
+ await authGetAnnouncements();
+ } else if (alarm.name === 'authUpdateCheck') {
+ await authCheckUpdate();
+ }
+ } catch (error) {
+ console.error('授权定时任务失败:', alarm.name, error);
+ }
+});
+
+// 安装上报 / 检查更新(后台「安装统计」自动记录)
+async function authCheckUpdate() {
+ const machineId = await getDeviceId();
+ const { licenseState } = await chrome.storage.local.get(['licenseState']);
+ const body = {
+ machine_id: machineId,
+ client_version: CLIENT_VERSION,
+ device_info: navigator.userAgent.includes('Edg/') ? 'Edge 插件' : 'Chrome 插件'
+ };
+ if (licenseState && licenseState.cardKey) body.card_key = licenseState.cardKey;
+
+ const res = await authFetch('/api/public/installations/check-update', {
+ method: 'POST',
+ body,
+ needKey: true
+ });
+
+ if (res.ok && res.data) {
+ const updateInfo = {
+ updateAvailable: !!res.data.update_available,
+ updateRequired: !!res.data.update_required,
+ latestVersion: res.data.latest_version || '',
+ downloadUrl: res.data.download_url || '',
+ changelog: res.data.changelog || '',
+ isForce: !!res.data.is_force,
+ checkedAt: Date.now()
+ };
+ await chrome.storage.local.set({ updateInfo });
+ console.log('更新检查:', updateInfo.updateAvailable ? `发现 ${updateInfo.latestVersion}` : '已是最新');
+ }
+ return res;
+}
+
+// 心跳:更新「最后上线」时间
+async function authHeartbeat() {
+ const machineId = await getDeviceId();
+ const { licenseState } = await chrome.storage.local.get(['licenseState']);
+ const body = {
+ machine_id: machineId,
+ client_version: CLIENT_VERSION
+ };
+ if (licenseState && licenseState.cardKey) body.card_key = licenseState.cardKey;
+
+ return authFetch('/api/public/installations/heartbeat', {
+ method: 'POST',
+ body,
+ needKey: true
+ });
+}
+
+// 授权验证:cardKey 存在则校验并更新缓存;首次调用即绑定设备
+async function authVerify(cardKey) {
+ const key = String(cardKey || '').trim();
+ if (!key) return { ok: false, message: '卡密不能为空' };
+
+ const machineId = await getDeviceId();
+ const res = await authFetch('/api/public/verify', {
+ method: 'POST',
+ body: {
+ card_key: key,
+ machine_id: machineId,
+ client_version: CLIENT_VERSION,
+ device_info: '小红书评论插件 ' + CLIENT_VERSION
+ },
+ needKey: true
+ });
+
+ if (!res.ok) {
+ // 网络/服务异常时不清除现有授权,仅报错
+ return { ok: false, message: res.message || '验证失败' };
+ }
+
+ const data = res.data || {};
+ const licenseState = {
+ valid: !!data.valid,
+ cardKey: key,
+ message: data.message || '',
+ expireTime: data.expire_time || '',
+ isPermanent: !!data.is_permanent,
+ remainingDays: data.remaining_days ?? null,
+ boundAt: data.bound_at || '',
+ verifiedAt: Date.now()
+ };
+ await chrome.storage.local.set({ licenseState });
+ console.log('授权验证:', licenseState.valid ? '有效' : '无效', licenseState.message);
+ return { ok: true, licenseState };
+}
+
+// 拉取公告并缓存
+async function authGetAnnouncements() {
+ const res = await authFetch('/api/public/announcements');
+ if (res.ok && res.data) {
+ const announcements = {
+ list: res.data.list || [],
+ popup: res.data.popup || [],
+ fetchedAt: Date.now()
+ };
+ await chrome.storage.local.set({ announcements });
+ return { ok: true, announcements };
+ }
+ return { ok: false, message: res.message || '拉取公告失败' };
+}
+
+// 读取授权状态(valid授权 > 1天试用 > 锁定)
+async function authGetStatus() {
+ const { licenseState, trialStartedAt } = await chrome.storage.local.get(['licenseState', 'trialStartedAt']);
+
+ if (licenseState && licenseState.valid) {
+ return {
+ state: 'authorized',
+ license: {
+ expireTime: licenseState.expireTime,
+ isPermanent: licenseState.isPermanent,
+ remainingDays: licenseState.remainingDays,
+ cardKey: licenseState.cardKey
+ }
+ };
+ }
+
+ const start = trialStartedAt || Date.now();
+ if (!trialStartedAt) {
+ await chrome.storage.local.set({ trialStartedAt: start });
+ }
+ const remainingMs = start + TRIAL_MS - Date.now();
+ if (remainingMs > 0) {
+ return {
+ state: 'trial',
+ remainingMs,
+ remainingHours: Math.max(1, Math.ceil(remainingMs / 3600000)),
+ trialEndAt: start + TRIAL_MS
+ };
+ }
+ return { state: 'locked' };
+}
+
+// 创建购买订单(contact_info 带机器码 → 支付成功后系统自动发卡绑定)
+async function authCreateOrder(payType) {
+ const pay = payType === 'wxpay' ? 'wxpay' : 'alipay';
+
+ // 取项目第一个套餐
+ const typesRes = await authFetch(`/api/public/projects/${AUTH_PROJECT_ID}/card-types`);
+ if (!typesRes.ok || !Array.isArray(typesRes.data) || typesRes.data.length === 0) {
+ return { ok: false, message: '暂无在售套餐,请稍后再试' };
+ }
+ const cardType = typesRes.data[0];
+
+ const machineId = await getDeviceId();
+ const res = await authFetch('/api/public/orders', {
+ method: 'POST',
+ body: {
+ project_id: AUTH_PROJECT_ID,
+ card_type_id: cardType.id,
+ amount: Number(cardType.price),
+ pay_type: pay,
+ contact_info: `MACHINE:${machineId}`
+ }
+ });
+
+ if (!res.ok || !res.data) {
+ return { ok: false, message: res.message || '创建订单失败' };
+ }
+
+ // 记录进行中的订单,便于侧边栏恢复轮询
+ const pendingOrder = {
+ orderNo: res.data.order_no,
+ payType: pay,
+ amount: Number(cardType.price),
+ packageName: cardType.name || '',
+ createdAt: Date.now()
+ };
+ await chrome.storage.local.set({ pendingOrder });
+
+ return { ok: true, order: pendingOrder, payUrl: res.data.pay_url };
+}
+
+// 查询订单(支付+自动发卡后返回 card_key;拿到卡密自动激活)
+async function authQueryOrder(orderNo) {
+ const res = await authFetch(`/api/public/orders/query?order_no=${encodeURIComponent(orderNo)}`);
+ if (!res.ok || !res.data) {
+ return { ok: false, message: res.message || '订单查询失败' };
+ }
+
+ const data = res.data;
+ const result = {
+ ok: true,
+ paid: data.status === 'paid' || !!data.card_key,
+ status: data.status,
+ cardKey: data.card_key || '',
+ expireTime: data.expire_time || ''
+ };
+
+ // 自动发卡成功 → 自动激活绑定本机
+ if (result.cardKey) {
+ const verifyRes = await authVerify(result.cardKey);
+ result.activated = !!(verifyRes.ok && verifyRes.licenseState && verifyRes.licenseState.valid);
+ result.licenseState = verifyRes.licenseState;
+ if (result.activated) {
+ await chrome.storage.local.remove(['pendingOrder']);
+ }
+ }
+ return result;
+}
+
+// 监听来自内容脚本的消息
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (request.action === 'getCurrentNote') {
+ handleGetCurrentNote(request, sendResponse);
+ return true; // 保持消息通道开放
+ }
+
+ if (request.action === 'saveNote') {
+ handleSaveNote(request.noteData, sendResponse);
+ return true; // 保持消息通道开放
+ }
+
+ if (request.action === 'callAiApi') {
+ handleCallAiApi(request, sendResponse);
+ return true; // 保持消息通道开放
+ }
+
+ if (request.action === 'getNotes') {
+ handleGetNotes(sendResponse);
+ return true;
+ }
+
+ if (request.action === 'deleteNote') {
+ handleDeleteNote(request.noteId, sendResponse);
+ return true;
+ }
+
+ // ===== 授权系统消息 =====
+ if (request.action === 'authStatus') {
+ authGetStatus().then((status) => sendResponse({ ok: true, status }));
+ return true;
+ }
+
+ if (request.action === 'authActivate') {
+ authVerify(request.cardKey).then((res) => sendResponse(res));
+ return true;
+ }
+
+ if (request.action === 'authCreateOrder') {
+ authCreateOrder(request.payType).then((res) => sendResponse(res));
+ return true;
+ }
+
+ if (request.action === 'authQueryOrder') {
+ authQueryOrder(request.orderNo).then((res) => sendResponse(res));
+ return true;
+ }
+
+ if (request.action === 'authGetAnnouncements') {
+ authGetAnnouncements().then((res) => sendResponse(res)).catch((error) => {
+ sendResponse({ ok: false, message: error.message });
+ });
+ return true;
+ }
+
+ if (request.action === 'authCheckUpdate') {
+ authCheckUpdate()
+ .then(async () => {
+ const { updateInfo } = await chrome.storage.local.get(['updateInfo']);
+ sendResponse({ ok: true, updateInfo: updateInfo || null });
+ })
+ .catch((error) => sendResponse({ ok: false, message: error.message }));
+ return true;
+ }
+});
+
+// 调用自定义模型(OpenAI 兼容接口),由后台执行以规避页面 CORS / 混合内容限制
+async function handleCallAiApi(request, sendResponse) {
+ const apiKey = String(request.apiKey || '').trim();
+ if (!apiKey) {
+ sendResponse({ ok: false, error: '未配置API密钥' });
+ return;
+ }
+
+ const url = buildChatCompletionsUrl(request.baseUrl);
+ const model = String(request.model || '').trim() || DEFAULT_AI.model;
+ const prompt = String(request.prompt || '');
+ const maxTokens = Number(request.maxTokens) > 0 ? Number(request.maxTokens) : 300;
+ const timeoutMs = Number(request.timeoutMs) > 0 ? Number(request.timeoutMs) : 90000;
+
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+ try {
+ const body = buildChatBody(model, prompt, maxTokens);
+
+ const sendOnce = async (payload) => {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${apiKey}`
+ },
+ body: JSON.stringify(payload),
+ signal: controller.signal
+ });
+ let data = {};
+ try {
+ data = await response.json();
+ } catch (parseError) {
+ data = {};
+ }
+ return { response: response, data: data };
+ };
+
+ let result = await sendOnce(body);
+
+ // 400/422:端点不认某些参数,按报错修正后重试一次
+ if (!result.response.ok && (result.response.status === 400 || result.response.status === 422)) {
+ const message = (result.data && result.data.error && result.data.error.message) || '';
+ let repaired = false;
+
+ if (body.thinking && /thinking|unknown|unrecognized|extra|invalid|not allowed|parameter/i.test(message)) {
+ delete body.thinking;
+ repaired = true;
+ }
+ if (body.max_tokens && /max_tokens/i.test(message) && /max_completion_tokens|unknown|unrecognized|invalid|not allowed/i.test(message)) {
+ body.max_completion_tokens = body.max_tokens;
+ delete body.max_tokens;
+ repaired = true;
+ }
+
+ if (repaired) {
+ console.log('参数不被端点接受,修正后重试:', message);
+ result = await sendOnce(body);
+ }
+ }
+
+ if (!result.response.ok) {
+ sendResponse({
+ ok: false,
+ status: result.response.status,
+ error: (result.data && result.data.error && result.data.error.message) || `HTTP ${result.response.status}`,
+ url: url,
+ model: model
+ });
+ return;
+ }
+
+ let content = extractChatContent(result.data);
+
+ // 正文为空:基本是 max_tokens 被思考过程吃掉 → 提高上限重试一次
+ if (!content) {
+ const choice = result.data && Array.isArray(result.data.choices) ? result.data.choices[0] : null;
+ const truncated = !!(choice && choice.finish_reason === 'length');
+ const hasReasoning = !!(choice && choice.message && typeof choice.message.reasoning_content === 'string' && choice.message.reasoning_content.length > 0);
+
+ if (truncated || hasReasoning) {
+ body.max_tokens = Math.max(4096, maxTokens * 4);
+ console.log('正文为空,提高 max_tokens 到', body.max_tokens, '重试');
+ result = await sendOnce(body);
+ if (result.response.ok) {
+ content = extractChatContent(result.data);
+ }
+ }
+
+ if (!content) {
+ sendResponse({
+ ok: false,
+ error: hasReasoning
+ ? '模型只返回了思考过程,没有返回正文(可提高 max_tokens 或换非思考型模型)'
+ : 'AI返回的内容为空',
+ url: url,
+ model: model
+ });
+ return;
+ }
+ }
+
+ sendResponse({ ok: true, content: content, url: url, model: model });
+ } catch (error) {
+ sendResponse({
+ ok: false,
+ error: error.name === 'AbortError' ? `请求超时(${Math.round(timeoutMs / 1000)}秒)` : error.message,
+ url: url,
+ model: model
+ });
+ } finally {
+ clearTimeout(timer);
+ }
+}
+
+async function handleGetCurrentNote(request, sendResponse) {
+ try {
+ sendResponse({
+ success: true,
+ message: '获取当前笔记成功'
+ });
+ } catch (error) {
+ console.error('获取当前笔记失败:', error);
+ sendResponse({ success: false, error: error.message });
+ }
+}
+
+async function handleSaveNote(request, sendResponse) {
+ try {
+ sendResponse({ success: true, message: '保存笔记成功' });
+ } catch (error) {
+ console.error('保存笔记失败:', error);
+ sendResponse({ success: false, error: error.message });
+ }
+}
+
+async function handleGetNotes(sendResponse) {
+ try {
+ sendResponse({
+ success: true,
+ notes: [],
+ tags: []
+ });
+ } catch (error) {
+ console.error('获取笔记失败:', error);
+ sendResponse({ success: false, error: error.message });
+ }
+}
+
+async function handleDeleteNote(request, sendResponse) {
+ try {
+ sendResponse({ success: true, message: '删除笔记成功' });
+ } catch (error) {
+ console.error('删除笔记失败:', error);
+ sendResponse({ success: false, error: error.message });
+ }
+}
+
+// 存储变化监听
+chrome.storage.onChanged.addListener((changes, namespace) => {
+ if (namespace === 'local') {
+ console.log('存储数据发生变化:', changes);
+ }
+});
+
+// 标签页更新监听
+chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status === 'complete' && tab.url && tab.url.includes('xiaohongshu.com')) {
+ // 当小红书页面加载完成时,可以在这里执行一些操作
+ console.log('小红书页面加载完成');
+ }
+});
\ No newline at end of file
diff --git a/content.css b/content.css
new file mode 100644
index 0000000..3634bf6
--- /dev/null
+++ b/content.css
@@ -0,0 +1,423 @@
+/* 小红书笔记收藏按钮样式 */
+.xhs-save-btn {
+ position: fixed !important;
+ top: 50% !important;
+ right: 20px !important;
+ z-index: 9999 !important;
+ background: linear-gradient(135deg, #ff6b6b, #ff8e8e) !important;
+ color: white !important;
+ border: none !important;
+ border-radius: 50px !important;
+ padding: 12px 16px !important;
+ font-size: 14px !important;
+ cursor: pointer !important;
+ box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3) !important;
+ transition: all 0.3s ease !important;
+ font-family: 'Segoe UI', sans-serif !important;
+ transform: translateY(-50%) !important;
+}
+
+.xhs-save-btn:hover {
+ background: linear-gradient(135deg, #ff5252, #ff7b7b) !important;
+ box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4) !important;
+ transform: translateY(-50%) scale(1.05) !important;
+}
+
+.xhs-save-btn.saved {
+ background: linear-gradient(135deg, #4caf50, #66bb6a) !important;
+}
+
+.xhs-save-btn.saved:hover {
+ background: linear-gradient(135deg, #45a049, #5db85d) !important;
+}
+
+.xhs-toast {
+ position: fixed !important;
+ top: 20px !important;
+ right: 20px !important;
+ z-index: 10000 !important;
+ background: rgba(0, 0, 0, 0.8) !important;
+ color: white !important;
+ padding: 12px 20px !important;
+ border-radius: 6px !important;
+ font-size: 14px !important;
+ animation: slideIn 0.3s ease !important;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.xhs-tag-modal {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ background: rgba(0, 0, 0, 0.5) !important;
+ z-index: 10001 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+}
+
+.xhs-tag-modal-content {
+ background: white !important;
+ padding: 24px !important;
+ border-radius: 8px !important;
+ width: 400px !important;
+ max-width: 90% !important;
+ max-height: 80% !important;
+ overflow-y: auto !important;
+}
+
+.xhs-tag-modal h3 {
+ margin: 0 0 16px 0 !important;
+ color: #333 !important;
+ font-size: 18px !important;
+}
+
+.xhs-tag-input {
+ width: 100% !important;
+ padding: 8px 12px !important;
+ border: 1px solid #ddd !important;
+ border-radius: 4px !important;
+ margin-bottom: 12px !important;
+ font-size: 14px !important;
+}
+
+.xhs-tag-list {
+ display: flex !important;
+ flex-wrap: wrap !important;
+ gap: 8px !important;
+ margin-bottom: 16px !important;
+}
+
+.xhs-tag-item {
+ padding: 6px 12px !important;
+ background: #f0f0f0 !important;
+ border: 1px solid #ddd !important;
+ border-radius: 16px !important;
+ font-size: 12px !important;
+ cursor: pointer !important;
+ transition: all 0.3s ease !important;
+}
+
+.xhs-tag-item:hover {
+ background: #e0e0e0 !important;
+}
+
+.xhs-tag-item.selected {
+ background: #ff6b6b !important;
+ color: white !important;
+ border-color: #ff6b6b !important;
+}
+
+.xhs-modal-actions {
+ display: flex !important;
+ gap: 8px !important;
+ justify-content: flex-end !important;
+}
+
+.xhs-modal-btn {
+ padding: 8px 16px !important;
+ border: none !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ font-size: 14px !important;
+ transition: all 0.3s ease !important;
+}
+
+.xhs-primary-btn {
+ background: #ff6b6b !important;
+ color: white !important;
+}
+
+.xhs-primary-btn:hover {
+ background: #ff5252 !important;
+}
+
+.xhs-secondary-btn {
+ background: #f0f0f0 !important;
+ color: #333 !important;
+}
+
+.xhs-secondary-btn:hover {
+ background: #e0e0e0 !important;
+}
+
+/* 进度条样式 */
+.xhs-progress-bar {
+ position: fixed !important;
+ top: 20px !important;
+ left: 50% !important;
+ transform: translateX(-50%) !important;
+ z-index: 10002 !important;
+ background: rgba(0, 0, 0, 0.9) !important;
+ color: white !important;
+ padding: 16px 24px !important;
+ border-radius: 8px !important;
+ font-size: 14px !important;
+ min-width: 300px !important;
+ opacity: 0 !important;
+ transition: opacity 0.3s ease !important;
+}
+
+.xhs-progress-bar.show {
+ opacity: 1 !important;
+}
+
+.xhs-progress-container {
+ text-align: center !important;
+}
+
+.xhs-progress-text {
+ margin-bottom: 12px !important;
+ font-weight: 600 !important;
+ color: #4caf50 !important;
+}
+
+.xhs-progress-bar-inner {
+ width: 100% !important;
+ height: 6px !important;
+ background: rgba(255, 255, 255, 0.2) !important;
+ border-radius: 3px !important;
+ overflow: hidden !important;
+ margin-bottom: 12px !important;
+}
+
+.xhs-progress-fill {
+ height: 100% !important;
+ background: linear-gradient(90deg, #4caf50, #66bb6a) !important;
+ width: 0% !important;
+ transition: width 0.3s ease !important;
+}
+
+.xhs-progress-stats {
+ display: flex !important;
+ justify-content: space-between !important;
+ font-size: 12px !important;
+ color: #ccc !important;
+}
+
+.xhs-progress-tip {
+ text-align: center !important;
+ font-size: 11px !important;
+ color: #888 !important;
+ margin-bottom: 8px !important;
+ font-style: italic !important;
+}
+
+.xhs-stop-btn {
+ background: #f44336 !important;
+ color: white !important;
+ border: none !important;
+ padding: 6px 12px !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ font-size: 12px !important;
+ transition: background 0.3s ease !important;
+}
+
+.xhs-stop-btn:hover {
+ background: #d32f2f !important;
+}
+
+.xhs-progress-actions {
+ display: flex !important;
+ gap: 8px !important;
+ justify-content: center !important;
+ margin-top: 8px !important;
+}
+
+.xhs-test-stop-btn {
+ background: #ff9800 !important;
+ color: white !important;
+ border: none !important;
+ padding: 6px 12px !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ font-size: 12px !important;
+ transition: background 0.3s ease !important;
+}
+
+.xhs-test-stop-btn:hover {
+ background: #f57c00 !important;
+}
+
+/* 自定义评论管理弹窗样式 */
+.xhs-comment-modal {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ background: rgba(0, 0, 0, 0.5) !important;
+ z-index: 10003 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+}
+
+.xhs-comment-modal-content {
+ background: white !important;
+ padding: 24px !important;
+ border-radius: 8px !important;
+ width: 500px !important;
+ max-width: 90% !important;
+ max-height: 80% !important;
+ overflow-y: auto !important;
+}
+
+.xhs-comment-modal h3 {
+ margin: 0 0 16px 0 !important;
+ color: #333 !important;
+ font-size: 18px !important;
+ text-align: center !important;
+}
+
+.xhs-comment-input-area {
+ display: flex !important;
+ gap: 8px !important;
+ margin-bottom: 16px !important;
+}
+
+.xhs-comment-input {
+ flex: 1 !important;
+ padding: 8px 12px !important;
+ border: 1px solid #ddd !important;
+ border-radius: 4px !important;
+ font-size: 14px !important;
+}
+
+.xhs-add-comment-btn {
+ padding: 8px 16px !important;
+ background: #4caf50 !important;
+ color: white !important;
+ border: none !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ font-size: 14px !important;
+ transition: background 0.3s ease !important;
+ white-space: nowrap !important;
+}
+
+.xhs-add-comment-btn:hover {
+ background: #45a049 !important;
+}
+
+.xhs-comment-list {
+ margin-bottom: 16px !important;
+}
+
+.xhs-comment-list-header {
+ font-weight: 600 !important;
+ color: #333 !important;
+ margin-bottom: 8px !important;
+ font-size: 14px !important;
+}
+
+.xhs-comment-items {
+ max-height: 200px !important;
+ overflow-y: auto !important;
+ border: 1px solid #eee !important;
+ border-radius: 4px !important;
+ padding: 8px !important;
+}
+
+.xhs-comment-item {
+ display: flex !important;
+ justify-content: space-between !important;
+ align-items: center !important;
+ padding: 8px !important;
+ border-bottom: 1px solid #f0f0f0 !important;
+ transition: background 0.2s ease !important;
+}
+
+.xhs-comment-item:last-child {
+ border-bottom: none !important;
+}
+
+.xhs-comment-item:hover {
+ background: #f9f9f9 !important;
+}
+
+.xhs-comment-text {
+ flex: 1 !important;
+ margin-right: 8px !important;
+ font-size: 14px !important;
+ color: #333 !important;
+ word-break: break-all !important;
+}
+
+.xhs-delete-comment-btn {
+ padding: 4px 8px !important;
+ background: #f44336 !important;
+ color: white !important;
+ border: none !important;
+ border-radius: 3px !important;
+ cursor: pointer !important;
+ font-size: 12px !important;
+ transition: background 0.3s ease !important;
+ white-space: nowrap !important;
+}
+
+.xhs-delete-comment-btn:hover {
+ background: #d32f2f !important;
+}
+
+.xhs-no-comments {
+ text-align: center !important;
+ color: #999 !important;
+ font-style: italic !important;
+ padding: 20px !important;
+}
+
+/* 评论模式切换按钮样式 */
+.xhs-comment-mode-btn {
+ background: #2196f3 !important;
+ color: white !important;
+ border: none !important;
+ padding: 6px 12px !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ font-size: 12px !important;
+ transition: all 0.3s ease !important;
+ position: relative !important;
+}
+
+.xhs-comment-mode-btn:hover {
+ background: #1976d2 !important;
+ transform: scale(1.05) !important;
+}
+
+.xhs-comment-mode-btn.xhs-secondary-btn {
+ background: #ff9800 !important;
+}
+
+.xhs-comment-mode-btn.xhs-secondary-btn:hover {
+ background: #f57c00 !important;
+}
+
+/* 清理内存按钮样式 */
+.xhs-cleanup-btn {
+ background: #9c27b0 !important;
+ color: white !important;
+ border: none !important;
+ padding: 6px 12px !important;
+ border-radius: 4px !important;
+ cursor: pointer !important;
+ font-size: 12px !important;
+ transition: background 0.3s ease !important;
+}
+
+.xhs-cleanup-btn:hover {
+ background: #7b1fa2 !important;
+}
\ No newline at end of file
diff --git a/content.js b/content.js
new file mode 100644
index 0000000..0469e1c
--- /dev/null
+++ b/content.js
@@ -0,0 +1,2529 @@
+// 内容脚本 - 在小红书页面上添加收藏功能
+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.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 = `
+
+
选择标签
+
+
+ ${tags.map(tag => `
+
+ ${tag.name} (${tag.count || 0})
+
+ `).join('')}
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // 绑定事件
+ const tagInput = modal.querySelector('.xhs-tag-input');
+ const tagItems = modal.querySelectorAll('.xhs-tag-item');
+ const cancelBtn = modal.querySelector('#cancel-save');
+ const confirmBtn = modal.querySelector('#confirm-save');
+
+ let selectedTags = [];
+
+ // 标签选择
+ tagItems.forEach(item => {
+ item.addEventListener('click', () => {
+ const tagName = item.dataset.tag;
+ if (item.classList.contains('selected')) {
+ item.classList.remove('selected');
+ selectedTags = selectedTags.filter(tag => tag !== tagName);
+ } else {
+ item.classList.add('selected');
+ selectedTags.push(tagName);
+ }
+ });
+ });
+
+ // 新标签输入
+ tagInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ const newTag = tagInput.value.trim();
+ if (newTag && !selectedTags.includes(newTag)) {
+ selectedTags.push(newTag);
+
+ // 添加到标签列表显示
+ const tagList = modal.querySelector('.xhs-tag-list');
+ const newTagElement = document.createElement('div');
+ newTagElement.className = 'xhs-tag-item selected';
+ newTagElement.dataset.tag = newTag;
+ newTagElement.textContent = newTag;
+ tagList.appendChild(newTagElement);
+
+ newTagElement.addEventListener('click', () => {
+ newTagElement.classList.remove('selected');
+ selectedTags = selectedTags.filter(tag => tag !== newTag);
+ newTagElement.remove();
+ });
+
+ tagInput.value = '';
+ }
+ }
+ });
+
+ // 取消按钮
+ cancelBtn.addEventListener('click', () => {
+ modal.remove();
+ });
+
+ // 确认保存
+ confirmBtn.addEventListener('click', async () => {
+ await this.saveNoteWithTags(selectedTags);
+ modal.remove();
+ });
+
+ // 点击背景关闭
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ modal.remove();
+ }
+ });
+ }
+
+ async saveNoteWithTags(selectedTags) {
+ try {
+ // 保存笔记
+ const result = await chrome.storage.local.get(['savedNotes', 'tags']);
+ const savedNotes = result.savedNotes || [];
+ const tags = result.tags || [];
+
+ // 添加标签到笔记
+ this.currentNoteData.tags = selectedTags;
+ savedNotes.push(this.currentNoteData);
+
+ // 更新标签计数
+ selectedTags.forEach(tagName => {
+ const existingTag = tags.find(tag => tag.name === tagName);
+ if (existingTag) {
+ existingTag.count = (existingTag.count || 0) + 1;
+ } else {
+ tags.push({ name: tagName, count: 1 });
+ }
+ });
+
+ // 保存到存储
+ await chrome.storage.local.set({
+ savedNotes,
+ tags
+ });
+
+ this.updateSaveButtonState(true);
+ this.showToast(`已收藏 ${selectedTags.length > 0 ? '并添加标签' : ''}`);
+
+ } catch (error) {
+ console.error('保存笔记失败:', error);
+ this.showToast('保存失败,请重试');
+ }
+ }
+
+ showToast(message) {
+ const toast = document.createElement('div');
+ toast.className = 'xhs-toast';
+ toast.textContent = message;
+ document.body.appendChild(toast);
+
+ setTimeout(() => {
+ toast.remove();
+ }, 3000);
+ }
+
+ setupMessageListener() {
+ console.log('设置消息监听器...');
+
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ console.log('收到消息:', request, '来自:', sender);
+
+ if (request.action === 'getCurrentNote') {
+ console.log('处理获取当前笔记请求');
+ sendResponse({
+ noteData: this.currentNoteData,
+ isNotePage: this.currentNoteData !== null
+ });
+ }
+
+ if (request.action === 'autoClickNotes') {
+ console.log('收到自动点击请求,关键词:', request.keyword);
+
+ // 立即响应,表示消息已收到
+ sendResponse({ success: true, message: '开始自动点击' });
+
+ // 开始自动点击
+ this.startAutoClickNotes();
+ }
+
+ if (request.action === 'testElementDetection') {
+ console.log('收到测试元素检测请求');
+
+ // 测试元素检测
+ const primarySelector = '#global > div.main-container > div.with-side-bar.main-content > div > div > div.search-layout__main > div.feeds-container > section';
+ let primaryElements = [];
+
+ try {
+ primaryElements = document.querySelectorAll(primarySelector);
+ } catch (error) {
+ console.log('主要选择器测试失败:', error);
+ }
+
+ const elements = this.findNoteElements();
+ const visibleElements = this.findVisibleNoteElements();
+
+ // 分析第一个元素的详细结构
+ let elementAnalysis = [];
+ if (elements.length > 0) {
+ const firstElement = elements[0];
+ const links = firstElement.querySelectorAll('a');
+ const buttons = firstElement.querySelectorAll('button, [role="button"]');
+ const clickableDivs = firstElement.querySelectorAll('div[class*="click"], div[class*="expand"], div[class*="more"]');
+
+ // 专门检测目标链接
+ const targetLink = firstElement.querySelector('div > a.cover.mask.ld');
+
+ elementAnalysis = {
+ tagName: firstElement.tagName,
+ className: firstElement.className,
+ noteIndex: firstElement.dataset.noteIndex,
+ selector: firstElement.dataset.selector,
+ targetLink: targetLink ? {
+ href: targetLink.href,
+ text: targetLink.textContent.trim(),
+ className: targetLink.className,
+ found: true
+ } : {
+ found: false,
+ message: '未找到目标链接 div > a.cover.mask.ld'
+ },
+ links: Array.from(links).map(link => ({
+ href: link.href,
+ text: link.textContent.trim(),
+ className: link.className
+ })),
+ buttons: Array.from(buttons).map(btn => ({
+ text: btn.textContent.trim(),
+ className: btn.className,
+ role: btn.getAttribute('role')
+ })),
+ clickableDivs: Array.from(clickableDivs).map(div => ({
+ text: div.textContent.trim(),
+ className: div.className
+ }))
+ };
+ }
+
+ const result = {
+ success: true,
+ primarySelector: primarySelector,
+ primaryElementsCount: primaryElements.length,
+ totalElements: elements.length,
+ visibleElements: visibleElements.length,
+ elementDetails: elements.slice(0, 5).map(el => ({
+ tagName: el.tagName,
+ className: el.className,
+ hasLink: !!el.querySelector('a[href*="/explore/"], a[href*="/discovery/"], a[href*="/note/"]'),
+ hasImage: !!el.querySelector('img'),
+ textLength: el.textContent ? el.textContent.trim().length : 0
+ })),
+ firstElementAnalysis: elementAnalysis
+ };
+
+ console.log('测试结果:', result);
+ sendResponse(result);
+ }
+
+ // 返回true表示异步响应
+ return true;
+ });
+
+ console.log('消息监听器设置完成');
+ }
+
+ // 自动点击笔记功能
+ async startAutoClickNotes() {
+ console.log('开始自动点击笔记...');
+
+ try {
+ // 检查内存使用情况
+ this.checkMemoryUsage();
+
+ // 设置自动点击状态
+ this.isAutoClicking = true;
+
+ // 显示进度条
+ this.showProgressBar();
+
+ // 等待页面完全加载
+ await this.waitForPageLoad();
+
+ // 高亮显示检测到的元素(限制数量)
+ this.highlightDetectedElements();
+
+ // 开始自动点击
+ await this.autoClickVisibleNotes();
+
+ } catch (error) {
+ console.error('自动点击过程中发生错误:', error);
+ this.showToast('自动点击出错,已停止');
+ } finally {
+ // 隐藏进度条
+ this.hideProgressBar();
+
+ // 重置状态
+ this.isAutoClicking = false;
+
+ // 执行清理
+ this.performMemoryCleanup();
+ }
+ }
+
+ // 等待页面加载完成
+ async waitForPageLoad() {
+ return new Promise((resolve, reject) => {
+ let attempts = 0;
+ const maxAttempts = 20; // 最多等待10秒
+
+ const checkNotes = () => {
+ // 检查是否被停止
+ if (!this.isAutoClicking) {
+ reject(new Error('等待页面加载被停止'));
+ return;
+ }
+
+ attempts++;
+ const notes = this.findNoteElements();
+ if (notes.length > 0) {
+ resolve();
+ } else if (attempts >= maxAttempts) {
+ reject(new Error('等待页面加载超时'));
+ } else {
+ const timerId = setTimeout(checkNotes, 500);
+ this.cleanupTimers.add(timerId);
+ }
+ };
+ checkNotes();
+ });
+ }
+
+ // 查找笔记元素
+ findNoteElements() {
+ console.log('开始查找笔记元素...');
+
+ // 使用用户提供的精确选择器,查找所有笔记
+ const baseSelector = '#global > div.main-container > div.with-side-bar.main-content > div > div > div.search-layout__main > div.feeds-container > section';
+
+ console.log('使用基础选择器:', baseSelector);
+
+ // 查找所有section元素
+ const allSections = document.querySelectorAll(baseSelector);
+ console.log(`找到 ${allSections.length} 个section元素`);
+
+ if (allSections.length > 0) {
+ // 为每个section添加索引信息
+ const notesWithIndex = Array.from(allSections).map((section, index) => {
+ section.dataset.noteIndex = index + 1;
+ section.dataset.selector = `${baseSelector}:nth-child(${index + 1})`;
+ return section;
+ });
+
+ console.log('笔记元素已添加索引信息');
+ return notesWithIndex;
+ }
+
+ // 如果主要选择器失败,使用备用选择器
+ console.log('主要选择器未找到元素,尝试备用选择器...');
+ const fallbackSelectors = [
+ '.note-item',
+ '.search-item',
+ '.feed-item',
+ '.card-item',
+ '.note-card',
+ '.search-note-item',
+ '.search-result-item',
+ '.note-list-item'
+ ];
+
+ for (const selector of fallbackSelectors) {
+ try {
+ const elements = document.querySelectorAll(selector);
+ if (elements.length > 0) {
+ console.log(`备用选择器 "${selector}" 找到 ${elements.length} 个元素`);
+ return Array.from(elements);
+ }
+ } catch (error) {
+ console.log(`备用选择器 "${selector}" 无效:`, error);
+ }
+ }
+
+ console.log('未找到任何笔记元素');
+ return [];
+ }
+
+ // 自动点击可见的笔记
+ async autoClickVisibleNotes() {
+ let totalClicked = 0;
+ let hasMoreNotes = true;
+ let totalFound = 0;
+ let consecutiveFailures = 0; // 连续失败计数
+ const maxConsecutiveFailures = 5; // 最大连续失败次数
+
+ try {
+ while (hasMoreNotes && this.isAutoClicking) {
+ // 检查是否被停止
+ if (!this.isAutoClicking) {
+ console.log('自动点击被停止');
+ break;
+ }
+
+ // 检查连续失败次数
+ if (consecutiveFailures >= maxConsecutiveFailures) {
+ console.log(`连续失败${maxConsecutiveFailures}次,停止自动点击`);
+ this.showToast('连续失败次数过多,已停止自动点击');
+ break;
+ }
+
+ // 查找当前可见的笔记
+ const visibleNotes = this.findVisibleNoteElements();
+
+ if (visibleNotes.length === 0) {
+ console.log('没有找到更多笔记');
+ break;
+ }
+
+ totalFound += visibleNotes.length;
+ console.log(`找到 ${visibleNotes.length} 个可见笔记`);
+
+ // 更新进度条
+ this.updateProgress(totalClicked, totalFound);
+
+ // 限制并发处理的笔记数量
+ const notesToProcess = visibleNotes.slice(0, this.maxConcurrentNotes);
+
+ // 依次点击每个笔记
+ for (let i = 0; i < notesToProcess.length && this.isAutoClicking; i++) {
+ const note = notesToProcess[i];
+
+ try {
+ // 检查是否已经点击过
+ if (note.dataset.clicked === 'true') {
+ continue;
+ }
+
+ // 检查是否正在处理
+ if (this.processingNotes.has(note)) {
+ continue;
+ }
+
+ // 标记为正在处理
+ this.processingNotes.add(note);
+
+ // 标记为已点击
+ note.dataset.clicked = 'true';
+
+ // 点击笔记(展开内容)
+ await this.clickNote(note);
+
+ // 检查是否被停止
+ if (!this.isAutoClicking) {
+ console.log('自动点击被停止');
+ break;
+ }
+
+ totalClicked++;
+ consecutiveFailures = 0; // 重置失败计数
+
+ // 更新进度条
+ this.updateProgress(totalClicked, totalFound);
+
+ console.log(`已点击第 ${totalClicked} 个笔记`);
+
+ // 等待内容展开,增加等待时间
+ await this.sleep(2000);
+
+ } catch (error) {
+ console.error('点击笔记失败:', error);
+ consecutiveFailures++;
+ } finally {
+ // 清理处理状态
+ this.processingNotes.delete(note);
+ }
+ }
+
+ // 检查是否被停止
+ if (!this.isAutoClicking) {
+ console.log('自动点击被停止');
+ break;
+ }
+
+ // 滚动页面加载更多笔记
+ try {
+ hasMoreNotes = await this.scrollToLoadMore();
+
+ if (hasMoreNotes) {
+ // 等待新内容加载
+ await this.sleep(2000);
+ }
+ } catch (error) {
+ console.error('滚动加载失败:', error);
+ hasMoreNotes = false;
+ }
+
+ // 添加内存清理
+ if (totalClicked % 10 === 0) {
+ this.performMemoryCleanup();
+ }
+ }
+ } catch (error) {
+ console.error('自动点击过程中发生错误:', error);
+ } finally {
+ // 清理所有资源
+ this.cleanup();
+ }
+
+ console.log(`自动点击完成,总共点击了 ${totalClicked} 个笔记`);
+ this.showToast(`自动点击完成,共点击 ${totalClicked} 个笔记`);
+ }
+
+ // 查找可见的笔记元素
+ findVisibleNoteElements() {
+ const notes = this.findNoteElements();
+ const visibleNotes = [];
+
+ for (const note of notes) {
+ if (this.isElementVisible(note) && note.dataset.clicked !== 'true') {
+ visibleNotes.push(note);
+ }
+ }
+
+ return visibleNotes;
+ }
+
+ // 检查元素是否可见
+ isElementVisible(element) {
+ const rect = element.getBoundingClientRect();
+ const windowHeight = window.innerHeight;
+
+ // 元素在视口内且有一定高度
+ return rect.top < windowHeight && rect.bottom > 0 && rect.height > 50;
+ }
+
+ // 点击笔记
+ async clickNote(noteElement) {
+ console.log('尝试点击笔记元素:', noteElement);
+ console.log('元素标签:', noteElement.tagName, '类名:', noteElement.className);
+
+ // 获取用户选择的点击模式
+ const shouldExpand = await this.getClickMode();
+ console.log('点击模式 - 展开内容:', shouldExpand);
+
+ if (shouldExpand) {
+ // 展开内容模式
+ await this.expandNoteContent(noteElement);
+ } else {
+ // 简单点击模式
+ await this.simpleClickNote(noteElement);
+ }
+ }
+
+ // 获取用户选择的点击模式
+ async getClickMode() {
+ try {
+ const result = await chrome.storage.local.get(['expandContent']);
+ return result.expandContent !== false; // 默认展开内容
+ } catch (error) {
+ console.log('获取点击模式失败,使用默认模式:', error);
+ return true;
+ }
+ }
+
+ // 展开笔记内容
+ async expandNoteContent(noteElement) {
+ console.log(`处理笔记 ${noteElement.dataset.noteIndex || '未知'}:`, noteElement.dataset.selector || '未知选择器');
+
+ // 专门查找并点击 div > a.cover.mask.ld 链接
+ const targetLink = noteElement.querySelector('div > a.cover.mask.ld');
+
+ if (targetLink) {
+ console.log('找到目标链接:', targetLink.href);
+ console.log('链接文本:', targetLink.textContent.trim());
+ console.log('链接类名:', targetLink.className);
+
+ try {
+ // 滚动到链接位置,确保可见
+ targetLink.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await this.sleep(500);
+
+ // 点击链接
+ targetLink.click();
+ console.log('成功点击目标链接');
+
+ // 等待内容加载并执行评论交互流程
+ await this.executeCommentInteraction();
+
+ return;
+ } catch (error) {
+ console.log('点击目标链接失败:', error);
+ }
+ } else {
+ console.log('未找到目标链接 div > a.cover.mask.ld');
+ }
+
+ // 如果没找到目标链接,尝试其他方法
+ console.log('尝试备用点击方法...');
+
+ // 查找笔记中的其他可点击元素,但排除链接,只选择能展开内容的元素
+ const clickableElements = noteElement.querySelectorAll('[role="button"], [tabindex], [onclick], [class*="click"], [class*="expand"], [class*="open"], [class*="toggle"], [class*="more"], [class*="view"]');
+ console.log(`找到 ${clickableElements.length} 个其他可点击元素`);
+
+ // 优先选择看起来像能展开内容的元素
+ let bestTarget = null;
+
+ for (const clickable of clickableElements) {
+ const className = clickable.className.toLowerCase();
+ const text = clickable.textContent.toLowerCase();
+
+ // 检查是否看起来像展开按钮
+ if (className.includes('expand') || className.includes('open') ||
+ className.includes('click') || className.includes('toggle') ||
+ className.includes('more') || className.includes('view') ||
+ text.includes('展开') || text.includes('更多') || text.includes('查看') ||
+ text.includes('显示') || text.includes('详情')) {
+ bestTarget = clickable;
+ console.log('找到展开按钮:', clickable.tagName, clickable.className, clickable.textContent);
+ break;
+ }
+ }
+
+ // 如果没有找到明确的展开按钮,尝试查找其他可点击元素(但绝对不是链接)
+ if (!bestTarget) {
+ // 查找按钮、div等非链接元素
+ const buttonElements = noteElement.querySelectorAll('button, [role="button"], div[class*="click"], div[class*="expand"], div[class*="more"]');
+ console.log(`找到 ${buttonElements.length} 个按钮元素`);
+
+ if (buttonElements.length > 0) {
+ bestTarget = buttonElements[0];
+ console.log('选择按钮元素:', bestTarget.tagName, bestTarget.className);
+ }
+ }
+
+ // 尝试点击选中的目标
+ if (bestTarget) {
+ try {
+ console.log('尝试点击备用目标元素:', bestTarget.tagName, bestTarget.className);
+
+ // 滚动到元素位置,确保可见
+ bestTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await this.sleep(500);
+
+ // 模拟点击
+ bestTarget.click();
+ console.log('成功点击备用目标元素');
+
+ // 等待内容展开并执行评论交互流程
+ await this.executeCommentInteraction();
+
+ return;
+ } catch (error) {
+ console.log('点击备用目标元素失败:', error);
+ }
+ }
+
+ // 如果所有方法都失败了,尝试点击整个笔记元素(但阻止默认行为)
+ try {
+ console.log('尝试点击整个笔记元素(阻止默认行为)');
+
+ // 滚动到笔记位置
+ noteElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await this.sleep(500);
+
+ // 模拟鼠标点击,但阻止默认行为
+ this.simulateClickWithPreventDefault(noteElement);
+ console.log('成功点击整个笔记元素');
+
+ // 等待内容展开并执行评论交互流程
+ await this.executeCommentInteraction();
+
+ } catch (error) {
+ console.log('点击整个笔记元素失败:', error);
+ }
+ }
+
+ // 简单点击笔记
+ async simpleClickNote(noteElement) {
+ try {
+ console.log(`执行简单点击模式 - 笔记 ${noteElement.dataset.noteIndex || '未知'}`);
+
+ // 优先查找并点击目标链接
+ const targetLink = noteElement.querySelector('div > a.cover.mask.ld');
+
+ if (targetLink) {
+ console.log('找到目标链接,执行简单点击');
+
+ // 滚动到链接位置
+ targetLink.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await this.sleep(300);
+
+ // 点击链接
+ targetLink.click();
+ console.log('成功点击目标链接(简单模式)');
+
+ // 等待内容加载并执行评论交互流程
+ await this.executeCommentInteraction();
+
+ return;
+ }
+
+ // 如果没有找到目标链接,执行原来的简单点击逻辑
+ console.log('未找到目标链接,执行备用简单点击');
+
+ // 滚动到笔记位置
+ noteElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await this.sleep(300);
+
+ // 直接点击整个笔记元素,但阻止默认行为
+ this.simulateClickWithPreventDefault(noteElement);
+ console.log('成功执行备用简单点击');
+
+ // 短暂等待
+ await this.sleep(500);
+
+ } catch (error) {
+ console.log('简单点击失败:', error);
+ }
+ }
+
+ // 模拟鼠标点击
+ simulateClick(element) {
+ const rect = element.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ // 创建鼠标事件
+ const clickEvent = new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ clientX: centerX,
+ clientY: centerY
+ });
+
+ element.dispatchEvent(clickEvent);
+ }
+
+ // 模拟鼠标点击并阻止默认行为
+ simulateClickWithPreventDefault(element) {
+ const rect = element.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ const clickEvent = new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ clientX: centerX,
+ clientY: centerY
+ });
+
+ element.dispatchEvent(clickEvent);
+ clickEvent.preventDefault(); // 阻止默认行为
+ }
+
+ // 显示进度条
+ showProgressBar() {
+ // 移除已存在的进度条
+ this.hideProgressBar();
+
+ const progressBar = document.createElement('div');
+ progressBar.className = 'xhs-progress-bar';
+ progressBar.innerHTML = `
+
+
🤖 自动点击进行中...
+
+
+ 已点击: 0
+ 总计: 0
+ 内存: --
+
+
+ 💡 长按评论模式按钮可管理自定义评论
+
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(progressBar);
+
+ // 绑定停止按钮事件
+ const stopBtn = progressBar.querySelector('#xhs-stop-auto-click');
+ stopBtn.addEventListener('click', () => {
+ this.stopAutoClick();
+ });
+
+ // 绑定清理内存按钮事件
+ const cleanupBtn = progressBar.querySelector('#xhs-cleanup-memory');
+ cleanupBtn.addEventListener('click', () => {
+ this.emergencyMemoryCleanup();
+ });
+
+ // 绑定评论模式切换按钮事件
+ const commentModeBtn = progressBar.querySelector('#xhs-comment-mode');
+ commentModeBtn.addEventListener('click', () => {
+ this.toggleCommentMode();
+ });
+
+ // 长按显示自定义评论管理弹窗
+ let longPressTimer = null;
+ commentModeBtn.addEventListener('mousedown', () => {
+ longPressTimer = setTimeout(() => {
+ if (this.commentMode === 'custom') {
+ this.showCustomCommentModal();
+ }
+ }, 800); // 800ms长按
+ });
+
+ commentModeBtn.addEventListener('mouseup', () => {
+ if (longPressTimer) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ }
+ });
+
+ commentModeBtn.addEventListener('mouseleave', () => {
+ if (longPressTimer) {
+ clearTimeout(longPressTimer);
+ longPressTimer = null;
+ }
+ });
+
+ // 更新评论模式按钮显示
+ this.updateCommentModeButton();
+
+ // 添加动画效果
+ setTimeout(() => {
+ progressBar.classList.add('show');
+ }, 100);
+
+ // 启动内存使用更新
+ this.startMemoryUsageUpdate();
+ }
+
+ // 启动内存使用更新
+ startMemoryUsageUpdate() {
+ const updateMemory = () => {
+ if (!this.isAutoClicking) return;
+
+ const memoryElement = document.getElementById('xhs-memory-usage');
+ if (memoryElement) {
+ const stats = this.getMemoryStats();
+ if (stats) {
+ memoryElement.textContent = `内存: ${stats.usedMB}MB`;
+
+ // 根据内存使用情况改变颜色
+ if (stats.used > this.memoryThreshold) {
+ memoryElement.style.color = '#ff4444'; // 红色警告
+ } else if (stats.used > this.memoryThreshold * 0.7) {
+ memoryElement.style.color = '#ffaa00'; // 黄色警告
+ } else {
+ memoryElement.style.color = '#44ff44'; // 绿色正常
+ }
+ }
+ }
+ };
+
+ // 立即更新一次
+ updateMemory();
+
+ // 每5秒更新一次
+ const updateTimer = setInterval(updateMemory, 5000);
+ this.cleanupTimers.add(updateTimer);
+ }
+
+ // 高亮显示检测到的元素
+ highlightDetectedElements() {
+ // 先清理之前的高亮
+ this.cleanupHighlightedElements();
+
+ const elements = this.findNoteElements();
+ console.log(`高亮显示 ${elements.length} 个检测到的元素`);
+
+ // 限制高亮元素数量,避免内存问题
+ const maxHighlightElements = Math.min(elements.length, 20);
+ const elementsToHighlight = elements.slice(0, maxHighlightElements);
+
+ elementsToHighlight.forEach((el, index) => {
+ // 添加高亮样式
+ el.style.outline = '3px solid #4caf50';
+ el.style.outlineOffset = '2px';
+ el.style.position = 'relative';
+
+ // 添加序号标签
+ const label = document.createElement('div');
+ label.style.cssText = `
+ position: absolute;
+ top: -10px;
+ left: -10px;
+ background: #4caf50;
+ color: white;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: bold;
+ z-index: 10000;
+ pointer-events: none;
+ `;
+ label.textContent = index + 1;
+ el.appendChild(label);
+
+ // 设置清理定时器
+ const cleanupTimer = setTimeout(() => {
+ if (el.style.outline) {
+ el.style.outline = '';
+ el.style.outlineOffset = '';
+ el.style.position = '';
+ if (label.parentNode) {
+ label.remove();
+ }
+ }
+ // 从清理定时器集合中移除
+ this.cleanupTimers.delete(cleanupTimer);
+ }, 3000);
+
+ // 添加到清理定时器集合
+ this.cleanupTimers.add(cleanupTimer);
+ });
+
+ // 如果元素数量超过限制,显示提示
+ if (elements.length > maxHighlightElements) {
+ console.log(`高亮显示限制为${maxHighlightElements}个元素,避免内存问题`);
+ }
+ }
+
+ // 停止自动点击
+ stopAutoClick() {
+ console.log('用户停止自动点击');
+ this.isAutoClicking = false;
+
+ // 清理所有资源
+ this.cleanup();
+
+ // 显示停止提示
+ this.showToast('已停止自动点击');
+
+ console.log('自动点击已完全停止');
+ }
+
+ // 强制停止所有操作
+ forceStopAllOperations() {
+ console.log('强制停止所有操作...');
+
+ // 清理所有资源
+ this.cleanup();
+
+ console.log('所有操作已被强制停止');
+ }
+
+ // 获取内存使用统计
+ getMemoryStats() {
+ try {
+ if (performance.memory) {
+ return {
+ used: performance.memory.usedJSHeapSize,
+ total: performance.memory.totalJSHeapSize,
+ limit: performance.memory.jsHeapSizeLimit,
+ usedMB: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2),
+ totalMB: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2),
+ limitMB: (performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)
+ };
+ }
+ return null;
+ } catch (error) {
+ console.error('获取内存统计失败:', error);
+ return null;
+ }
+ }
+
+ // 隐藏进度条
+ hideProgressBar() {
+ const existingBar = document.querySelector('.xhs-progress-bar');
+ if (existingBar) {
+ existingBar.remove();
+ }
+ }
+
+ // 更新进度条
+ updateProgress(clicked, total) {
+ const clickedElement = document.getElementById('xhs-clicked-count');
+ const totalElement = document.getElementById('xhs-total-count');
+ const fillElement = document.querySelector('.xhs-progress-fill');
+
+ if (clickedElement) clickedElement.textContent = `已点击: ${clicked}`;
+ if (totalElement) totalElement.textContent = `总计: ${total}`;
+
+ if (fillElement && total > 0) {
+ const percentage = Math.min((clicked / total) * 100, 100);
+ fillElement.style.width = `${percentage}%`;
+ }
+ }
+
+ // 滚动加载更多内容
+ async scrollToLoadMore() {
+ // 检查是否被停止
+ if (!this.isAutoClicking) {
+ console.log('滚动加载被停止');
+ return false;
+ }
+
+ const scrollHeight = document.documentElement.scrollHeight;
+ const scrollTop = window.pageYOffset;
+ const clientHeight = document.documentElement.clientHeight;
+
+ // 如果已经滚动到底部,尝试加载更多
+ if (scrollTop + clientHeight >= scrollHeight - 100) {
+ // 滚动到底部
+ window.scrollTo(0, scrollHeight);
+
+ // 等待新内容加载
+ await this.sleep(2000);
+
+ // 检查是否被停止
+ if (!this.isAutoClicking) {
+ console.log('滚动加载被停止');
+ return false;
+ }
+
+ // 检查是否有新内容
+ const newScrollHeight = document.documentElement.scrollHeight;
+ return newScrollHeight > scrollHeight;
+ } else {
+ // 滚动到当前可见区域的底部
+ window.scrollBy(0, clientHeight);
+ return true;
+ }
+ }
+
+ // 延时函数
+ sleep(ms) {
+ return new Promise(resolve => {
+ // 如果已经被停止,立即返回
+ if (!this.isAutoClicking) {
+ resolve();
+ return;
+ }
+
+ const timeoutId = setTimeout(() => {
+ resolve();
+ // 清理定时器ID
+ this.cleanupTimers.delete(timeoutId);
+ }, ms);
+
+ // 添加到清理定时器集合
+ this.cleanupTimers.add(timeoutId);
+
+ // 创建一个检查停止的定时器
+ const checkStopInterval = setInterval(() => {
+ if (!this.isAutoClicking) {
+ clearTimeout(timeoutId);
+ clearInterval(checkStopInterval);
+ // 清理定时器ID
+ this.cleanupTimers.delete(timeoutId);
+ this.cleanupTimers.delete(checkStopInterval);
+ resolve();
+ }
+ }, 100);
+
+ // 添加到清理定时器集合
+ this.cleanupTimers.add(checkStopInterval);
+ });
+ }
+
+ // 阻止笔记元素内的所有链接跳转
+ 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') {
+ sentImage = await this.uploadCommentImage();
+ sentComment = '[图片]';
+ console.log('评论图片已添加');
+ } 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('未找到评论图片上传入口(小红书页面可能已改版)');
+ }
+
+ // 注入文件并触发 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 = `
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // 绑定事件
+ this.bindCustomCommentModalEvents(modal);
+
+ // 加载现有评论
+ this.loadCustomCommentsToModal(modal);
+ }
+
+ // 绑定自定义评论弹窗事件
+ bindCustomCommentModalEvents(modal) {
+ const addBtn = modal.querySelector('.xhs-add-comment-btn');
+ const input = modal.querySelector('.xhs-comment-input');
+ const closeBtn = modal.querySelector('#close-comment-modal');
+
+ // 添加评论
+ addBtn.addEventListener('click', async () => {
+ const comment = input.value.trim();
+ if (comment) {
+ const success = await this.addCustomComment(comment);
+ if (success) {
+ input.value = '';
+ this.loadCustomCommentsToModal(modal);
+ }
+ }
+ });
+
+ // 回车添加评论
+ input.addEventListener('keypress', async (e) => {
+ if (e.key === 'Enter') {
+ const comment = input.value.trim();
+ if (comment) {
+ const success = await this.addCustomComment(comment);
+ if (success) {
+ input.value = '';
+ this.loadCustomCommentsToModal(modal);
+ }
+ }
+ }
+ });
+
+ // 关闭弹窗
+ closeBtn.addEventListener('click', () => {
+ modal.remove();
+ });
+
+ // 点击背景关闭
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ modal.remove();
+ }
+ });
+ }
+
+ // 加载自定义评论到弹窗
+ async loadCustomCommentsToModal(modal) {
+ const commentItems = modal.querySelector('.xhs-comment-items');
+ const comments = await this.getAllCustomComments();
+
+ if (comments.length === 0) {
+ commentItems.innerHTML = '';
+ return;
+ }
+
+ commentItems.innerHTML = comments.map(comment => `
+
+ `).join('');
+
+ // 绑定删除按钮事件
+ const deleteBtns = modal.querySelectorAll('.xhs-delete-comment-btn');
+ deleteBtns.forEach(btn => {
+ btn.addEventListener('click', async () => {
+ const comment = btn.dataset.comment;
+ const success = await this.removeCustomComment(comment);
+ if (success) {
+ this.loadCustomCommentsToModal(modal);
+ }
+ });
+ });
+ }
+}
+
+// 初始化
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ new XHSNoteSaver();
+ });
+} else {
+ new XHSNoteSaver();
+}
\ No newline at end of file
diff --git a/icons/icon128.png b/icons/icon128.png
new file mode 100644
index 0000000000000000000000000000000000000000..55c87f8090e456f6fb5bea70f9447a00675e8e46
GIT binary patch
literal 2046
zcma)7`#;l*8~=Qa&s1aLxJ#_iQDSb(slztp-jv&vSfXMv(N}d0pQuGyE|sk-D#Bd9
zN-H&&F_&3{oHV8A)WI}4O^i6}KR7?UpZDwiyq?$dJU_kP+5USx)YclT1pq+J%hPS2
zg5jS~g%p+Vd`PGuFlnC$5vX}#G^r5EM+tre0I1Jhw|q!Nq1PmMh9&`kI{zoYPFi^s
z03fe;xe)?ShtJ%w&KcdU*P#nR46~^(W`rQhCC8=*-ud1{%b?1@^Ro9rW}pf|t+Mhi
z9Bi4hUvs$pZ#;oOz!NB*rI|?n@a@kVa9U%ADT4HQ$@TB|iwX|p&V0quc#DUv)hi;;
z(3?LVTPi2V-^ntF4m+L`UsF3$CnA;2;gUHs-SLmk#H{>o9V{lEX@R5%>0I*xT|aagohE8=Pmpl=_H=v5luDvoULY
zKRY-0rG}p7-+xyG3TIr!AEiFxXJ_CwC2@17h|;6y-R}-H;1~13ty>B{4qPk_vna^G
z`1OWK$5&H9iDo?!6PGNUK&Bw79CJg#e8&a&zCG-n88lR5l>mg}1kQI}D9PPX_@UBG`7NfTGBFO73(i}mV8
z0su`~Yq|WQ=R4TCIMH`^VVs&qw=BN=zuy=0S=IQ75cyIOVVrdb)L?tlR^B3I6Md&g
zIGcxojcM|gh9LRO_wYVeAxQtOZxgM)y9y+q2^phz_EAl(5OpRWaF0mSJ0G!>%bs0$OB+uL7wZrTI
zB}oz`2kfVwu-Zl=3fzX5UIMR9ggq#rDnpzHDb@b!F+lRFs$3svjUwLT{zXK=t4++H
z?&s>6DoszDr97V5pg*K~_El2_A+(Tcn`H6>)nb6EWUa%(eZGF^Z2Jh7m@C6-w$Z*g
zgq0l!g5NPCJmmWDnmyHu8GtfpMIDWMspsH#H40WGOEby2Y7Q`sDKs8pVQ&mM9_azr
z*J0O0`qcU(7XEM$T5?G_L`P4>qOvXrmwQbho8G$C0l>Rr>o3Se1oo6xfM68W(&IiL
z0(jpx+BGrQV1k;tj?oZ;(dxu4lsdn=jM5y~1j
zR|r)gO!|_V;4GG$rC?xFKtOP9bSOjhXVB>J-mArzK>BW~c#=B6hO$)ttma7RDv{BK
zS$43s1Fkf^zXRZv2yUY=6l~-elH2EE+pzPuVE_k?<15Tl{yTg8@Ef`jyWyFgf
z8gH4V8cz)YvPxoOA^fwwmwy%D
z3*(`z(p>N<%n&?UuMx$Jebld0T%m6gY&yaf971XQa{~uGF3Hyn^*<;Q0XDRpNE{Ev
z*9U47*SGv(Mi)ADS?R8$R&n;}Zn~fcq+h5MjT$rT@V?Po>!;6UymQfJM#KJBnmBho
z86p51xg@V|FTa7zDXB-n$EURJAhD7}+J!@(0qj)hS4|x^)#k(R&U)piY>Zgs83+L+
zg^V}{apR_U{9^xq@#{F@F8%kKe1jGoy4*1q@(k_HyAQTY@<3Y;sLM#-Bq<8+&HvBh
z=w5}>jDY#bOXJ6ue)PE{wAxyZxS$Ti5TpyRBIs%@g(*TXUbv)5vL$IkBS8K{*Tm027otM|djVB-4Vq-CB
zwihEQ>_i_QWcYA0?2p}&J@y(yy)cx_zA2MzPj%Wwf?}4v_0;Vx*y!g2JW_IG`K#7*
z*qNl4F>Sh*&xrOor^7hi5TC{9kluZ@xjm+Yq5?mqLDN1pi^drBt)qI{6m6}E$nmtj
zd-`wA&!bHPTLM1~6_k=+NFoUu^521RBAUhaF`YKWAKe*r+6qbUFY
literal 0
HcmV?d00001
diff --git a/icons/icon128.svg b/icons/icon128.svg
new file mode 100644
index 0000000..aba64c4
--- /dev/null
+++ b/icons/icon128.svg
@@ -0,0 +1,15 @@
+
diff --git a/icons/icon16.png b/icons/icon16.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5b83baa95c6ce0d00bffe43782759b86bfb574f
GIT binary patch
literal 376
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WCij$3p^r=85sBugD~Uq{1quc4ZNN%jv*HQ$qsD)L*MfLTWzKKC%i1@K&Rs;
zhtdFDfe-Qb;vUTJdi?Fbmf!!*qyMMO?fW0T`8tF22mhm)_GYdv1{Ws0b~yV@tibW~
zh5!HG+yDG;ufXhVsK>KuFN59%gOBzS4LnU7FEV<_++-*?&|pz@Pk?oE_`m=Avu^+Y
ze>7LXRpELA17ElzPs2@)%L1_+fi7DZ6=o+g9BI>3y2T*CX8HR|YDalOl8f{MDfU2)
zE>;1CkB*OAq!~8JJ}H@S-*HyrI)z&d6WYE=dGItaXlOY;V&M%aT*FZ8u&vqP%*!1I
z&K+LC_9wXPn}e8wXtR+JyMgJw<7%E9%wKEU)6}ZD6`os~x$ZFV_-wDboP*)}qoy6V
TqP7bILyN)F)z4*}Q$iB}3e=Fx
literal 0
HcmV?d00001
diff --git a/icons/icon16.svg b/icons/icon16.svg
new file mode 100644
index 0000000..a531234
--- /dev/null
+++ b/icons/icon16.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/icon32.png b/icons/icon32.png
new file mode 100644
index 0000000000000000000000000000000000000000..adaa98f3642e8ae913df9ca17f300985f4bcc13c
GIT binary patch
literal 619
zcmV-x0+juUP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0su)wK~z{r?U$iX
z#4r@bCy_`T3EL4A!J_Wg3kriFAt50+kPr+4aS9Xyg#b_B4}hQ$2m}&=JVSy+;`Zpb
z?y<6aW!tSg3}5m~w!U`n+iQEf1}@QjcY!);o5Fa|7(A!bUCJKq7R`32l2Dx_k@}=a
zQ5z&te5LcuWGQpOp-%oP4f2hC!FLM#y)1S1=8+moo!dg^`}nRz62|9Jr^@Uc5#2u$
zEw0LO2YNd0`0dN*90?#PHhd$xxi#0424|yVGSa!10wcH#92}eW&3)5ndEK-_za(Y?
z{q}oOAO#P9elewG1Gq*T%8ovGa~3MWW1%Vr(tRofQjiGX+OB}z4o)vkY;8AhgBlq?
zDIrt=_qG8p`^uI9y14#19%^KOlkl9{2G|^uA_;`@qYsaV8XK@5XdB@BVgh>`LX8X*
z!JONM7*vB7D{n>M%>b7e;`LyIX}}_c5iCMUlJSh{6=;cn1LGUO+OX#q_ccF8z>fhe
zK>QA1C)6}i&wy){vtR%pC26b!KNVnO_*pLYE5WaUVxJ1sGk}C;f-z~V13wjD{?q_}
zmhJxp6$mJb52ZjUYyjJi#rINw2f70No)joWnu0OFzv5{rFe%KrAW{!fux)S{N)6y3
zTIp=y8~b>c%&Cn@n!s@&OPwpNM5vym0lv_Qt%GS0d;xaT$X18N_7MO8002ovPDHLk
FV1g#C|7QRI
literal 0
HcmV?d00001
diff --git a/icons/icon32.svg b/icons/icon32.svg
new file mode 100644
index 0000000..0b4a616
--- /dev/null
+++ b/icons/icon32.svg
@@ -0,0 +1,9 @@
+
diff --git a/icons/icon48.png b/icons/icon48.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a6360083dfa1810a9b71c37c9b85999f46be0ab
GIT binary patch
literal 819
zcmV-31I+x1P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0?0{3K~!i%?OGvh
z6hRbSiA2_sbgvDnL4vyOUYd|#YMPLcG&qoLY7mGjsw%1wkRS<|ItUtpKvYy9&yXN1
zaXbBQcOEx8`}g+B9qdf9FL}AU`Sa)hH~-IG)4603n9cEBlTNnHM*h^K+3`PT83*yk
z_iPF`hq*JpU8MO*G0LZAl$*XN@qZPg?49JZW>f1%kxm}MQCqMtNc*PUy*S^)%t9-r9{Ezs6#6EOWb8{c|V^I4sM
zXD_QcZg7%f@ao$ze-V7Ytmx&S3&5fvdyjxK`0-P-q}(O=z!!pOn*x5CQBm#|Q+#XV?HH$smMZon-{zJ;2x=
z>jJRjgsYg~A`Aqu3iS1p2ipWNxtqWfQWwDTqo18gf8aW^=aDdy#{mQQ>dsxJ0%S`i
z>H@IqXb(RS;X0%5F&91o>*uNEk_9U>T`)Iao7r>yzyx<6gc0yZGWEZqHX{*2-F4jT;a}dxs
z2LXL^5YRUh{GVMj#KNRl&mU`sSST$t+13oP5Vte8%G(ab7z6DEM|Q3mLuZlZ7o`AP
za>Q5#oZnIa5ZmKay5_x2y+U~wq$Jwp&{*E-X_wu&wiFRRG^bD~$zS14+os%vt;C7u
x+&Aa`%)m!68sEp2zP8Z+rtJDu|Di{cTmy)Xmva-`XjlLM002ovPDHLkV1m;daccko
literal 0
HcmV?d00001
diff --git a/icons/icon48.svg b/icons/icon48.svg
new file mode 100644
index 0000000..d48ee3d
--- /dev/null
+++ b/icons/icon48.svg
@@ -0,0 +1,9 @@
+
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..86ae4e2
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,42 @@
+{
+ "manifest_version": 3,
+ "name": "小红书自动评论助手",
+ "version": "1.7.5",
+ "description": "右侧侧边栏常驻的自动评论助手,支持试用/卡密授权、固定池/自定义文字/图片/AI评论",
+ "permissions": [
+ "storage",
+ "unlimitedStorage",
+ "activeTab",
+ "scripting",
+ "tabs",
+ "sidePanel",
+ "alarms"
+ ],
+ "host_permissions": [
+ "https://www.xiaohongshu.com/*",
+ "https://*/*",
+ "http://*/*"
+ ],
+ "action": {
+ "default_title": "打开小红书自动评论助手侧边栏"
+ },
+ "side_panel": {
+ "default_path": "sidepanel.html"
+ },
+ "content_scripts": [
+ {
+ "matches": ["https://www.xiaohongshu.com/*"],
+ "js": ["content.js"],
+ "css": ["content.css"]
+ }
+ ],
+ "background": {
+ "service_worker": "background.js"
+ },
+ "icons": {
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
+ }
+}
\ No newline at end of file
diff --git a/sidepanel.css b/sidepanel.css
new file mode 100644
index 0000000..972a07a
--- /dev/null
+++ b/sidepanel.css
@@ -0,0 +1,1042 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background-color: #f5f5f5;
+ color: #333;
+}
+
+.container {
+ width: 100%;
+ min-height: 100vh;
+ background: white;
+ border-radius: 0;
+ overflow: hidden;
+}
+
+.header {
+ background: linear-gradient(135deg, #4caf50, #66bb6a);
+ color: white;
+ padding: 16px;
+ text-align: center;
+}
+
+.header h1 {
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.search-section {
+ padding: 16px;
+ border-bottom: 1px solid #eee;
+ background: #fafafa;
+}
+
+.search-form {
+ display: flex;
+ gap: 8px;
+}
+
+#keyword-input {
+ flex: 1;
+ padding: 10px 12px;
+ border: 2px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s ease;
+}
+
+#keyword-input:focus {
+ border-color: #4caf50;
+}
+
+#search-btn {
+ padding: 10px 16px;
+ background: linear-gradient(135deg, #4caf50, #66bb6a);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ min-width: 80px;
+}
+
+#search-btn:hover {
+ background: linear-gradient(135deg, #45a049, #5db85d);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
+}
+
+#search-btn:active {
+ transform: translateY(0);
+}
+
+.region-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.region-row label {
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ white-space: nowrap;
+}
+
+#region-select {
+ flex: 1;
+ padding: 8px 10px;
+ border: 2px solid #ddd;
+ border-radius: 6px;
+ font-size: 13px;
+ background: #fff;
+ color: #333;
+ outline: none;
+ cursor: pointer;
+ transition: border-color 0.3s ease;
+}
+
+#region-select:focus {
+ border-color: #4caf50;
+}
+
+.region-custom-input {
+ width: 100%;
+ margin-top: 8px;
+ padding: 8px 12px;
+ border: 2px solid #ddd;
+ border-radius: 6px;
+ font-size: 13px;
+ outline: none;
+ transition: border-color 0.3s ease;
+}
+
+.region-custom-input:focus {
+ border-color: #4caf50;
+}
+
+.region-custom-input[hidden] {
+ display: none;
+}
+
+.region-hint {
+ display: block;
+ margin-top: 6px;
+ color: #888;
+ font-size: 11px;
+ line-height: 1.4;
+}
+
+.auto-click-controls {
+ margin-top: 12px;
+ text-align: center;
+}
+
+.auto-click-btn {
+ padding: 8px 16px;
+ background: linear-gradient(135deg, #4caf50, #66bb6a);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ width: 100%;
+}
+
+.auto-click-btn:hover {
+ background: linear-gradient(135deg, #45a049, #5db85d);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
+}
+
+.auto-click-btn:active {
+ transform: translateY(0);
+}
+
+.test-btn {
+ padding: 8px 16px;
+ background: linear-gradient(135deg, #2196f3, #42a5f5);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ width: 100%;
+ margin-top: 8px;
+}
+
+.test-btn:hover {
+ background: linear-gradient(135deg, #1976d2, #1e88e5);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
+}
+
+.test-btn:active {
+ transform: translateY(0);
+}
+
+.click-mode-selector {
+ margin-top: 12px;
+ padding: 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.click-mode-selector label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ cursor: pointer;
+}
+
+.click-mode-selector input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ accent-color: #4caf50;
+}
+
+.click-mode-selector small {
+ display: block;
+ margin-top: 6px;
+ color: #666;
+ font-size: 11px;
+ line-height: 1.4;
+}
+
+.mode-selector {
+ margin-top: 12px;
+ padding: 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.mode-label {
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.mode-options {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.mode-option {
+ flex: 1 1 calc(50% - 8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 8px 6px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ background: #fff;
+ font-size: 13px;
+ color: #555;
+ cursor: pointer;
+ user-select: none;
+ transition: all 0.2s ease;
+}
+
+.mode-option input {
+ accent-color: #4caf50;
+}
+
+.mode-option:has(input:checked) {
+ border-color: #4caf50;
+ background: #e8f5e9;
+ color: #2e7d32;
+ font-weight: 600;
+}
+
+.mode-selector small {
+ display: block;
+ margin-top: 8px;
+ color: #666;
+ font-size: 11px;
+ line-height: 1.5;
+}
+
+.mode-selector small.warn {
+ color: #e65100;
+ background: #fff3e0;
+ border-left: 3px solid #ff9800;
+ padding: 4px 6px;
+ border-radius: 3px;
+}
+
+.comment-config {
+ margin-top: 12px;
+ padding: 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.comment-config label {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.comment-config input[type="text"] {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 13px;
+ margin-top: 4px;
+}
+
+.comment-config input[type="text"]:focus {
+ border-color: #4caf50;
+ outline: none;
+}
+
+.ai-config {
+ display: flex;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.api-key-input {
+ flex: 1;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 13px;
+}
+
+.api-key-input:focus {
+ border-color: #4caf50;
+ outline: none;
+}
+
+.test-ai-btn {
+ padding: 8px 12px;
+ background: linear-gradient(135deg, #9c27b0, #ba68c8);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.3s ease;
+ white-space: nowrap;
+}
+
+.test-ai-btn:hover {
+ background: linear-gradient(135deg, #7b1fa2, #9c27b0);
+ transform: translateY(-1px);
+}
+
+.test-result-row {
+ display: flex;
+ align-items: center;
+ min-height: 16px;
+ margin-top: 4px;
+}
+
+.action-status-row {
+ display: flex;
+ align-items: center;
+ min-height: 16px;
+ margin-top: 6px;
+ text-align: left;
+}
+
+.action-status {
+ font-size: 12px;
+ line-height: 1.4;
+ word-break: break-all;
+}
+
+.action-status.ok {
+ color: #2e7d32;
+}
+
+.action-status.fail {
+ color: #c62828;
+}
+
+.test-ai-result {
+ font-size: 12px;
+ line-height: 1.4;
+ word-break: break-all;
+}
+
+.test-ai-result.ok {
+ color: #2e7d32;
+}
+
+.test-ai-result.fail {
+ color: #c62828;
+}
+
+.ai-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 8px;
+ padding: 8px;
+ background: #f8f9fa;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.status-indicator {
+ font-size: 14px;
+}
+
+.status-text {
+ color: #666;
+}
+
+.comment-config small {
+ display: block;
+ margin-top: 6px;
+ color: #666;
+ font-size: 11px;
+ line-height: 1.4;
+}
+
+/* Prompt config styles */
+.prompt-config {
+ margin-top: 12px;
+ padding: 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.prompt-config label {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.prompt-textarea {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 13px;
+ font-family: inherit;
+ resize: vertical;
+}
+
+.prompt-textarea:focus {
+ border-color: #4caf50;
+ outline: none;
+}
+
+.prompt-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.save-prompt-btn {
+ padding: 8px 12px;
+ background: linear-gradient(135deg, #4caf50, #66bb6a);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.3s ease;
+ white-space: nowrap;
+}
+
+.save-prompt-btn:hover {
+ background: linear-gradient(135deg, #45a049, #5db85d);
+ transform: translateY(-1px);
+}
+
+.reset-prompt-btn {
+ padding: 8px 12px;
+ background: #f0f0f0;
+ color: #333;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.3s ease;
+ white-space: nowrap;
+}
+
+.reset-prompt-btn:hover {
+ background: #e0e0e0;
+}
+
+.info-section {
+ padding: 16px;
+ background: white;
+}
+
+/* 自定义文字库面板 */
+.text-comment-config,
+.image-comment-config {
+ margin-top: 12px;
+ padding: 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+}
+
+.text-comment-config label,
+.image-comment-config label {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.text-comment-config small,
+.image-comment-config small {
+ display: block;
+ margin-top: 6px;
+ color: #666;
+ font-size: 11px;
+ line-height: 1.4;
+}
+
+.panel-count {
+ font-size: 12px;
+ color: #666;
+ align-self: center;
+}
+
+/* 图片回复面板 */
+.image-file-input {
+ display: block;
+ width: 100%;
+ font-size: 12px;
+ color: #555;
+}
+
+.image-preview-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 10px;
+ min-height: 20px;
+}
+
+.image-preview-empty {
+ font-size: 12px;
+ color: #999;
+}
+
+.image-preview-item {
+ position: relative;
+ width: 64px;
+ height: 64px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid #ddd;
+ background: #fff;
+}
+
+.image-preview-item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.image-delete-btn {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 18px;
+ height: 18px;
+ border: none;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.55);
+ color: #fff;
+ font-size: 12px;
+ line-height: 18px;
+ text-align: center;
+ cursor: pointer;
+ padding: 0;
+}
+
+.image-delete-btn:hover {
+ background: rgba(0, 0, 0, 0.8);
+}
+
+/* 免费获取模型 KEY 引导横幅 */
+.ai-key-guide {
+ display: block;
+ margin-top: 12px;
+ padding: 10px 12px;
+ background: linear-gradient(135deg, #ff6f00, #ff9800);
+ color: #fff;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 600;
+ text-decoration: none;
+ text-align: center;
+ box-shadow: 0 2px 8px rgba(255, 152, 0, 0.4);
+ transition: all 0.2s ease;
+ animation: ai-guide-glow 2s ease-in-out infinite;
+}
+
+.ai-key-guide:hover {
+ background: linear-gradient(135deg, #e65100, #ff6f00);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(255, 111, 0, 0.5);
+ color: #fff;
+}
+
+.ai-key-guide-cta {
+ display: inline-block;
+ padding: 1px 8px;
+ margin: 0 2px;
+ background: #fff;
+ color: #e65100;
+ border-radius: 10px;
+ font-weight: 700;
+}
+
+@keyframes ai-guide-glow {
+ 0%, 100% { box-shadow: 0 2px 8px rgba(255, 152, 0, 0.4); }
+ 50% { box-shadow: 0 2px 16px rgba(255, 111, 0, 0.7); }
+}
+
+/* 评论记录面板 */
+.log-section {
+ padding: 16px;
+ border-bottom: 1px solid #eee;
+ background: #fff;
+}
+
+.log-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.log-header h3 {
+ color: #4caf50;
+ font-size: 16px;
+}
+
+.log-refresh-btn {
+ padding: 4px 12px;
+ background: #f0f0f0;
+ color: #333;
+ border: none;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.log-refresh-btn:hover {
+ background: #e0e0e0;
+}
+
+.log-stats {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.log-stat {
+ flex: 1;
+ text-align: center;
+ padding: 8px 4px;
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 6px;
+ font-size: 12px;
+ color: #666;
+}
+
+.log-stat strong {
+ display: block;
+ font-size: 18px;
+ color: #2e7d32;
+ margin-bottom: 2px;
+}
+
+.log-list {
+ max-height: 320px;
+ overflow-y: auto;
+ border: 1px solid #eee;
+ border-radius: 6px;
+}
+
+.log-empty {
+ padding: 20px 12px;
+ text-align: center;
+ color: #999;
+ font-size: 12px;
+}
+
+.log-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border-bottom: 1px solid #f2f2f2;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.log-item:last-child {
+ border-bottom: none;
+}
+
+.log-item:hover {
+ background: #f1f8e9;
+}
+
+.log-item.no-url {
+ cursor: default;
+ opacity: 0.75;
+}
+
+.log-item.no-url:hover {
+ background: #fff;
+}
+
+.log-thumb {
+ width: 40px;
+ height: 40px;
+ border-radius: 4px;
+ object-fit: cover;
+ flex-shrink: 0;
+ border: 1px solid #eee;
+ background: #fafafa;
+}
+
+.log-item-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.log-item-title {
+ font-size: 13px;
+ font-weight: 500;
+ color: #333;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.log-item-comment {
+ font-size: 12px;
+ color: #666;
+ margin-top: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.log-item-meta {
+ font-size: 11px;
+ color: #999;
+ margin-top: 2px;
+}
+
+.log-mode-tag {
+ display: inline-block;
+ padding: 0 5px;
+ border-radius: 3px;
+ background: #e8f5e9;
+ color: #2e7d32;
+ font-size: 10px;
+ margin-right: 4px;
+}
+
+.log-open-icon {
+ flex-shrink: 0;
+ color: #4caf50;
+ font-size: 14px;
+}
+
+/* 授权区块 */
+.auth-section {
+ padding: 12px 16px;
+ background: #fff;
+ border-bottom: 1px solid #eee;
+}
+
+.auth-status-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+
+.auth-badge {
+ flex-shrink: 0;
+ padding: 2px 10px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: 600;
+ background: #eee;
+ color: #666;
+}
+
+.auth-badge.authorized {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+
+.auth-badge.trial {
+ background: #fff3e0;
+ color: #e65100;
+}
+
+.auth-badge.locked {
+ background: #ffebee;
+ color: #c62828;
+}
+
+.auth-detail {
+ font-size: 12px;
+ color: #666;
+ line-height: 1.4;
+ word-break: break-all;
+}
+
+.auth-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.card-key-input {
+ flex: 1;
+ min-width: 0;
+ padding: 8px 10px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 12px;
+ outline: none;
+}
+
+.card-key-input:focus {
+ border-color: #4caf50;
+}
+
+.activate-btn,
+.buy-btn {
+ padding: 8px 12px;
+ border: none;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: all 0.2s ease;
+}
+
+.activate-btn {
+ background: linear-gradient(135deg, #4caf50, #66bb6a);
+ color: #fff;
+}
+
+.activate-btn:hover {
+ background: linear-gradient(135deg, #45a049, #5db85d);
+}
+
+.buy-btn {
+ background: linear-gradient(135deg, #ff9800, #ffb74d);
+ color: #fff;
+}
+
+.buy-btn:hover {
+ background: linear-gradient(135deg, #f57c00, #ff9800);
+}
+
+.pay-select-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.pay-select-label {
+ font-size: 12px;
+ color: #666;
+}
+
+.pay-btn {
+ padding: 6px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ background: #fff;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.pay-btn:hover {
+ border-color: #4caf50;
+ color: #2e7d32;
+}
+
+.pay-cancel {
+ color: #999;
+}
+
+.order-status {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #555;
+ word-break: break-all;
+}
+
+.order-status.ok {
+ color: #2e7d32;
+}
+
+.order-status.fail {
+ color: #c62828;
+}
+
+.announcement-box {
+ margin-top: 10px;
+ padding: 8px 10px;
+ background: #f1f8e9;
+ border: 1px solid #c5e1a5;
+ border-radius: 6px;
+ max-height: 140px;
+ overflow-y: auto;
+}
+
+.announcement-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: #33691e;
+ margin-bottom: 4px;
+}
+
+.announcement-item {
+ font-size: 12px;
+ color: #555;
+ line-height: 1.5;
+ padding: 4px 0;
+ border-bottom: 1px dashed #dcedc8;
+ word-break: break-all;
+}
+
+.announcement-item:last-child {
+ border-bottom: none;
+}
+
+.announcement-item .announcement-time {
+ color: #999;
+ font-size: 11px;
+ margin-right: 6px;
+}
+
+.info-content h3 {
+ color: #4caf50;
+ margin-bottom: 12px;
+ font-size: 16px;
+}
+
+.info-content ul, .info-content ol {
+ margin-left: 20px;
+ margin-bottom: 16px;
+}
+
+.info-content li {
+ margin-bottom: 8px;
+ line-height: 1.5;
+}
+
+.info-content strong {
+ color: #333;
+}
+
+.footer {
+ padding: 16px;
+ background: #f8f9fa;
+ border-top: 1px solid #eee;
+ text-align: center;
+}
+
+.version-info {
+ display: flex;
+ justify-content: space-between;
+ color: #666;
+ font-size: 12px;
+}
+
+/* 底部「获取最新版本」按钮 */
+.latest-version-bar {
+ padding: 16px;
+ background: #fff;
+ border-top: 1px solid #eee;
+ text-align: center;
+}
+
+.latest-version-btn {
+ display: block;
+ padding: 14px 16px;
+ background: linear-gradient(135deg, #ff2442, #ff5c57);
+ color: #fff;
+ border-radius: 10px;
+ font-size: 17px;
+ font-weight: 700;
+ text-decoration: none;
+ text-align: center;
+ letter-spacing: 1px;
+ box-shadow: 0 4px 14px rgba(255, 36, 66, 0.45);
+ transition: all 0.2s ease;
+ animation: latest-version-pulse 2.2s ease-in-out infinite;
+}
+
+.latest-version-btn:hover {
+ background: linear-gradient(135deg, #e01030, #ff2442);
+ transform: translateY(-2px);
+ box-shadow: 0 6px 18px rgba(255, 36, 66, 0.55);
+ color: #fff;
+}
+
+.latest-version-btn:active {
+ transform: translateY(0);
+}
+
+.latest-version-tip {
+ margin-top: 8px;
+ font-size: 11px;
+ color: #999;
+}
+
+@keyframes latest-version-pulse {
+ 0%, 100% { box-shadow: 0 4px 14px rgba(255, 36, 66, 0.45); }
+ 50% { box-shadow: 0 4px 22px rgba(255, 36, 66, 0.75); }
+}
\ No newline at end of file
diff --git a/sidepanel.html b/sidepanel.html
new file mode 100644
index 0000000..23bbeda
--- /dev/null
+++ b/sidepanel.html
@@ -0,0 +1,219 @@
+
+
+
+
+
+ 小红书自动评论助手
+
+
+
+
+
+
+
+
+ …
+ 正在检查授权状态…
+
+
+
+
+
+
+
+ 选择支付方式:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
选择地区后会自动拼进搜索关键词(如「咖啡 上海」),选择会自动保存
+
+
+
+
+
+
+
+
+ 勾选后会在点击笔记时展开内容,取消勾选则只简单点击
+
+
+
+
+
+ 🔑 还没有模型KEY?免费获取模型KEY → ai.88531.cn
+
+
+
+
+
+
+
+
+
+
示例:根据笔记内容生成引流评论...\\n笔记内容:{{content}}
+
+
+
+
+
+
+
+ 0 今日
+ 0 本月
+ 0 总计
+
+
+
仅显示最近 100 条(最多保留 300 条),点击记录可直接打开对应笔记
+
+
+
+
+
📋 功能说明
+
+ - 🔍 搜索功能:输入关键词自动搜索小红书内容
+ - 📍 地区筛选:选择地区自动拼进关键词,按地区限定搜索结果
+ - 🤖 自动点击:自动点击每篇笔记并发表评论
+ - 🧪 元素检测:测试页面元素是否被正确识别
+ - 💬 评论配置:自定义模型生成评论内容
+ - 📝 自定义文字:自己写文字库,循环发送
+ - 🖼️ 图片回复:上传图片,循环以图片评论
+ - 📊 评论记录:今日/本月/总计统计,点击记录直达笔记
+
+
+
⚡ 使用方法
+
+ - 点击工具栏图标打开右侧侧边栏
+ - 在小红书页面输入搜索关键词
+ - 点击"🔍 搜索"开始自动操作
+ - 或直接点击"🤖 自动点击笔记"
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sidepanel.js b/sidepanel.js
new file mode 100644
index 0000000..4377919
--- /dev/null
+++ b/sidepanel.js
@@ -0,0 +1,1285 @@
+// 默认自定义模型配置(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.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);
+ });
+
+ // 评论方式选择(固定评论池 / 自定义文字 / 图片 / 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);
+ }
+ }
+
+ // 保存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 = '暂无图片
';
+ 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 = '暂无评论记录,成功评论后会自动出现在这里
';
+ 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();
+});
\ No newline at end of file
自定义评论管理
+