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://img.shields.io/badge/版本-1.7.5-ff2442.svg)](https://dl.88531.cn/xhs.html) +[![平台](https://img.shields.io/badge/平台-Chrome%20%7C%20Edge-4caf50.svg)](#安装方法) +[![语言](https://img.shields.io/badge/语言-纯%20JavaScript-2196f3.svg)](#开发说明) + +一个专注于自动评论功能的浏览器插件:在浏览器**右侧侧边栏常驻**,输入关键词即可自动搜索小红书、逐篇点开笔记、发表评论,全程无需人工干预。 + +--- + +## 🌟 为什么选择我们 + +| 亮点 | 说明 | +|---|---| +| 🚀 **全流程自动化** | 输入关键词 → 自动开搜 → 自动点开每篇笔记 → 自动写评论 → 自动提交,一条龙跑完 | +| 🤖 **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 => ` +
+ ${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 @@ + + + + + + 小红书自动评论助手 + + + +
+
+

🤖 小红书自动评论助手

+
+ +
+
+ … + 正在检查授权状态… +
+
+ + + +
+ +
+ +
+ +
+
+ + +
+
+ + +
+ + 选择地区后会自动拼进搜索关键词(如「咖啡 上海」),选择会自动保存 +
+ +
+
+ + +
+ + 勾选后会在点击笔记时展开内容,取消勾选则只简单点击 +
+
+
评论方式
+
+ + + + +
+ 固定评论池 = 循环发预设短评;AI = 按每篇笔记内容生成 +
+
+ + +
+ + +
+ 改动即时保存已打开的小红书页面;一行一条,发完最后一条从头开始循环 +
+
+ + +
+ 图片会压缩后保存在本地(浏览器存储),大图建议先自行压缩;图片发完最后一条从头开始循环 +
+ + 🔑 还没有模型KEY?免费获取模型KEY → ai.88531.cn + +
+ + 任意 OpenAI 兼容接口均可(地址 + 模型名 + 密钥),改动即时保存 +
+ 🔴 + 未配置API密钥 +
+
+
+ + +
+ + +
+ 示例:根据笔记内容生成引流评论...\\n笔记内容:{{content}} +
+
+
+ +
+
+

📊 评论记录

+ +
+
+ 0 今日 + 0 本月 + 0 总计 +
+
+
暂无评论记录,成功评论后会自动出现在这里
+
+ 仅显示最近 100 条(最多保留 300 条),点击记录可直接打开对应笔记 +
+ +
+
+

📋 功能说明

+
    +
  • 🔍 搜索功能:输入关键词自动搜索小红书内容
  • +
  • 📍 地区筛选:选择地区自动拼进关键词,按地区限定搜索结果
  • +
  • 🤖 自动点击:自动点击每篇笔记并发表评论
  • +
  • 🧪 元素检测:测试页面元素是否被正确识别
  • +
  • 💬 评论配置:自定义模型生成评论内容
  • +
  • 📝 自定义文字:自己写文字库,循环发送
  • +
  • 🖼️ 图片回复:上传图片,循环以图片评论
  • +
  • 📊 评论记录:今日/本月/总计统计,点击记录直达笔记
  • +
+ +

⚡ 使用方法

+
    +
  1. 点击工具栏图标打开右侧侧边栏
  2. +
  3. 在小红书页面输入搜索关键词
  4. +
  5. 点击"🔍 搜索"开始自动操作
  6. +
  7. 或直接点击"🤖 自动点击笔记"
  8. +
+
+
+ + + +
+ + ⬇️ 获取最新版本 + +
版本更新 / 下载安装包请认准官方地址 dl.88531.cn
+
+
+ + + + 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