feat: v1.7.9 试用72h+100条额度+到期降级+24h优惠码;v1.7.8 移除图片回复模式

注:本提交由 Gitee v1.7.7 基线重建(原 GitHub 仓库账号暂停、源码包已丢),重建后经真机功能测试与 v1.7.9 行为一致。
This commit is contained in:
personal82555 committed 2026-10-17 22:01:13 +08:00
1 parent fae84ff6c7
commit 540f506c78
8 files changed
+355 -431

No files matched your search

+18 -12
View File
@@ -2,7 +2,7 @@
> 一键搜索 · 自动点击 · 智能评论 —— 小红书运营引流的效率神器 > 一键搜索 · 自动点击 · 智能评论 —— 小红书运营引流的效率神器
[![版本](https://img.shields.io/badge/版本-1.7.7-ff2442.svg)](https://dl.88531.cn/xhs.html) [![版本](https://img.shields.io/badge/版本-1.7.9-ff2442.svg)](https://dl.88531.cn/xhs.html)
[![平台](https://img.shields.io/badge/平台-Chrome%20%7C%20Edge-4caf50.svg)](#安装方法) [![平台](https://img.shields.io/badge/平台-Chrome%20%7C%20Edge-4caf50.svg)](#安装方法)
[![语言](https://img.shields.io/badge/语言-纯%20JavaScript-2196f3.svg)](#开发说明) [![语言](https://img.shields.io/badge/语言-纯%20JavaScript-2196f3.svg)](#开发说明)
@@ -15,12 +15,11 @@
| 亮点 | 说明 | | 亮点 | 说明 |
|---|---| |---|---|
| 🚀 **全流程自动化** | 输入关键词 → 自动开搜 → 自动点开每篇笔记 → 自动写评论 → 自动提交,一条龙跑完 | | 🚀 **全流程自动化** | 输入关键词 → 自动开搜 → 自动点开每篇笔记 → 自动写评论 → 自动提交,一条龙跑完 |
| 🤖 **4 种评论方式** | 固定评论池 / 自定义文字库 / **图片回复** / **AI 按笔记生成**,随心切换 | | 🤖 **3 种评论方式** | 固定评论池 / 自定义文字库 / **AI 按笔记生成**,随心切换 |
| 🧠 **AI 智能评论** | 接入任意 OpenAI 兼容接口,每篇笔记结合内容生成不同评论,告别复读机 | | 🧠 **AI 智能评论** | 接入任意 OpenAI 兼容接口,每篇笔记结合内容生成不同评论,告别复读机 |
| 🖼️ **图片评论** | 上传表情包/图片,每篇笔记自动发一张,多张循环,互动更自然 |
| 📍 **按地区搜索** | 选「上海」「杭州西湖区」自动拼进关键词,精准触达同城流量 | | 📍 **按地区搜索** | 选「上海」「杭州西湖区」自动拼进关键词,精准触达同城流量 |
| 📊 **评论数据统计** | 今日/本月/总计成功数一目了然,每条记录带标题+评论内容,**点击直达笔记** | | 📊 **评论数据统计** | 今日/本月/总计成功数一目了然,每条记录带标题+评论内容,**点击直达笔记** |
| 🔐 **免费试用 1 天** | 装完即用,满意再买;插件内支付宝/微信购买,**支付成功自动发卡自动激活** | | 🔐 **免费试用 3 天 · 100 条** | 72 小时 + 100 条评论额度,先到先停;到期**不锁死**:每天还能体验 3 条、记录全保留,24 小时内下单享限时优惠 |
| 📢 **公告推送** | 官方公告实时下发(弹窗/列表),运营消息不错过 | | 📢 **公告推送** | 官方公告实时下发(弹窗/列表),运营消息不错过 |
| 🛡️ **稳定可靠** | 元素检测+备用方案、失败自动重试、内存自动防护、页面结构变化可快速适配 | | 🛡️ **稳定可靠** | 元素检测+备用方案、失败自动重试、内存自动防护、页面结构变化可快速适配 |
| 🔒 **数据本地存储** | 评论库、图片、记录全部存在你自己的浏览器里,不上传第三方 | | 🔒 **数据本地存储** | 评论库、图片、记录全部存在你自己的浏览器里,不上传第三方 |
@@ -32,10 +31,9 @@
- **💬 评论方式**: - **💬 评论方式**:
- 固定评论池:循环发预设短评(👍 赞!/💯 太棒了!…) - 固定评论池:循环发预设短评(👍 赞!/💯 太棒了!…)
- 自定义文字:侧边栏文字库一行一条,循环发送 - 自定义文字:侧边栏文字库一行一条,循环发送
- 图片回复:上传图片自动压缩存本地,预览/删除管理(⚠️ 小红书网页版评论不支持图片,仅手机 App 支持,此模式仅供参考)
- AI 按笔记生成:提示词可自定义(`{{content}}` 占位符),失败自动降级 - AI 按笔记生成:提示词可自定义(`{{content}}` 占位符),失败自动降级
- **📊 评论记录**:成功评论自动记录,今日/本月/总计统计,点击记录打开对应笔记 - **📊 评论记录**:成功评论自动记录,今日/本月/总计统计,点击记录打开对应笔记
- **🔐 授权体系**:1 天试用(倒计时精确到秒)、卡密激活、插件内购买自动发卡 - **🔐 授权体系**:72 小时试用 + 100 条额度(先到先停)、到期降级每天 3 条、卡密激活、插件内购买自动发卡
- **🧪 元素检测**:一键测试页面元素识别情况,方便小红书改版后快速排查 - **🧪 元素检测**:一键测试页面元素识别情况,方便小红书改版后快速排查
## 🚀 安装方法 ## 🚀 安装方法
@@ -53,8 +51,8 @@
- 再点一次图标可收起 - 再点一次图标可收起
### 授权与购买 ### 授权与购买
1. 安装后自动进入 **1 天免费试用**(侧边栏顶部显示精确到秒的倒计时) 1. 安装后自动进入 **72 小时免费试用**(含 100 条评论额度,侧边栏顶部倒计时精确到秒、额度实时显示)
2. 试用结束后需激活:输入卡密点「激活」,或点「购买授权」 2. 试用结束后**不硬锁**:自动转为体验模式(评论记录与配置全保留,每天 3 条额度,用完次日恢复);此时点「购买授权」下单自动带 24 小时限时优惠码(`XHS24H`,8 折),也可输入卡密点「激活」
3. 购买流程:点购买 → 选支付宝/微信 → 新页面完成支付 → **支付成功自动发卡并激活生效**,无需手动输卡密 3. 购买流程:点购买 → 选支付宝/微信 → 新页面完成支付 → **支付成功自动发卡并激活生效**,无需手动输卡密
4. 已购用户也可在 [购买中心](https://auth.88531.cn/shop) 用订单号查询卡密,回来手动激活 4. 已购用户也可在 [购买中心](https://auth.88531.cn/shop) 用订单号查询卡密,回来手动激活
5. 授权绑定本机浏览器,换设备请联系管理员重置 5. 授权绑定本机浏览器,换设备请联系管理员重置
@@ -66,10 +64,9 @@
> 说明:小红书网页版没有原生地区筛选,地区以关键词形式参与搜索(实际搜索词为「关键词 地区」)。 > 说明:小红书网页版没有原生地区筛选,地区以关键词形式参与搜索(实际搜索词为「关键词 地区」)。
### 评论方式(四选一,改完立即生效) ### 评论方式(三选一,改完立即生效)
- **固定评论池**:循环发预设短评,不调用模型 - **固定评论池**:循环发预设短评,不调用模型
- **自定义文字**:文字库一行一条,循环发送 - **自定义文字**:文字库一行一条,循环发送
- **图片回复**:每篇笔记发一张图片,多张循环
- **AI 按笔记生成**:每篇笔记调用模型按内容生成 - **AI 按笔记生成**:每篇笔记调用模型按内容生成
未配置对应内容时(如选了文字模式但库是空的),选项下方会给出橙色提示。 未配置对应内容时(如选了文字模式但库是空的),选项下方会给出橙色提示。
@@ -107,10 +104,19 @@
## 📜 版本历史 ## 📜 版本历史
当前版本:**1.7.7** 当前版本:**1.7.9**
<details> <details>
<summary>展开查看完整变更记录(v1.2.0 ~ v1.7.7)</summary> <summary>展开查看完整变更记录(v1.2.0 ~ v1.7.9)</summary>
### 1.7.9
- **试用策略升级「72 小时 + 100 条」**:时间与条数先到先停——拉长试用必须配用量上限,避免长时间白嫖评论量
- **到期不再硬锁,改降级体验**:自动转为体验模式,评论记录/文字库/模型配置全部保留,每天仍可发 3 条(用完次日恢复)
- **24 小时限时优惠**:到期后侧边栏出现限时优惠条(码 `XHS24H`,8 折,倒计时精确到秒),一键复制;点「购买授权」下单自动抵扣,优惠码异常时自动按原价下单、绝不挡购买
- 试用倒计时新增「额度剩 N 条」实时显示;剩余 ≤10 条时页面会提示
### 1.7.8
- **移除图片回复模式**:小红书网页版评论框本身不支持发图片(仅手机 App 支持),该模式一直无法真正发送,已从侧边栏选项、配置面板与发送逻辑中整体移除;评论方式精简为「固定评论池 / 自定义文字 / AI 按笔记生成」三种,旧版本残留的 image 模式会自动回退为固定评论池
### 1.7.7 ### 1.7.7
- **明确图片评论限制**:小红书网页版评论框不支持发图片(仅手机 App 支持)。图片模式发送失败时页面弹出明确提示(只弹一次),侧边栏选项下方同步橙色警告,建议改用「自定义文字」或「AI」模式 - **明确图片评论限制**:小红书网页版评论框不支持发图片(仅手机 App 支持)。图片模式发送失败时页面弹出明确提示(只弹一次),侧边栏选项下方同步橙色警告,建议改用「自定义文字」或「AI」模式
+147 -18
View File
@@ -16,8 +16,33 @@ const DEFAULT_AI = {
const AUTH_BASE = 'https://auth.88531.cn'; const AUTH_BASE = 'https://auth.88531.cn';
const AUTH_PROJECT_ID = 3; const AUTH_PROJECT_ID = 3;
const AUTH_API_KEY = 'ak_bbf83b76df8219388d7f60b9e67c7db2'; const AUTH_API_KEY = 'ak_bbf83b76df8219388d7f60b9e67c7db2';
const CLIENT_VERSION = '1.7.7'; const CLIENT_VERSION = '1.7.9';
const TRIAL_MS = 24 * 60 * 60 * 1000; // 未授权试用1天 const TRIAL_MS = 72 * 60 * 60 * 1000; // 试用 72 小时(3 天)
const TRIAL_QUOTA = 100; // 试用期最多 100 条评论(时间/条数先到先停,防止白嫖薅量)
const GRACE_DAILY = 3; // 试用到期不硬锁:每天保留 3 条体验额度
const PROMO = { code: 'XHS24H', percent: 20, windowMs: 24 * 60 * 60 * 1000 }; // 到期后 24 小时限时优惠(8 折)
// ===== 试用用量统计(时间/条数额度 + 到期降级都读这里) =====
function todayStr() {
const d = new Date();
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
}
async function getUsage() {
const { trialUsage } = await chrome.storage.local.get(['trialUsage']);
const u = trialUsage && typeof trialUsage === 'object' ? trialUsage : {};
return {
total: Number(u.total) || 0,
day: u.day || '',
dayCount: Number(u.dayCount) || 0,
quotaHitAt: Number(u.quotaHitAt) || 0,
trialEndedAt: Number(u.trialEndedAt) || 0
};
}
async function saveUsage(u) {
await chrome.storage.local.set({ trialUsage: u });
}
// 把用户填写的地址规范化为完整的 chat/completions 端点 // 把用户填写的地址规范化为完整的 chat/completions 端点
function buildChatCompletionsUrl(baseUrl) { function buildChatCompletionsUrl(baseUrl) {
@@ -156,7 +181,7 @@ async function authOnInstalled() {
const result = await chrome.storage.local.get(['trialStartedAt']); const result = await chrome.storage.local.get(['trialStartedAt']);
if (!result.trialStartedAt) { if (!result.trialStartedAt) {
await chrome.storage.local.set({ trialStartedAt: Date.now() }); await chrome.storage.local.set({ trialStartedAt: Date.now() });
console.log('试用期开始(1天)'); console.log('试用期开始(3 天 · 上限 ' + TRIAL_QUOTA + ' 条)');
} }
} catch (error) { } catch (error) {
console.error('初始化试用期失败:', error); console.error('初始化试用期失败:', error);
@@ -318,7 +343,7 @@ async function authGetAnnouncements() {
return { ok: false, message: res.message || '拉取公告失败' }; return { ok: false, message: res.message || '拉取公告失败' };
} }
// 读取授权状态(valid授权 > 1天试用 > 锁定) // 读取授权状态(valid授权 > 72 小时试用 > 到期降级体验,不硬锁)
async function authGetStatus() { async function authGetStatus() {
const { licenseState, trialStartedAt } = await chrome.storage.local.get(['licenseState', 'trialStartedAt']); const { licenseState, trialStartedAt } = await chrome.storage.local.get(['licenseState', 'trialStartedAt']);
@@ -338,16 +363,99 @@ async function authGetStatus() {
if (!trialStartedAt) { if (!trialStartedAt) {
await chrome.storage.local.set({ trialStartedAt: start }); await chrome.storage.local.set({ trialStartedAt: start });
} }
const remainingMs = start + TRIAL_MS - Date.now(); const usage = await getUsage();
if (remainingMs > 0) { const trialEndAt = start + TRIAL_MS;
const timeUp = Date.now() >= trialEndAt;
const quotaReached = usage.quotaHitAt > 0;
// 试用期内:时间没到 且 评论额度没用完
if (!timeUp && !quotaReached) {
return { return {
state: 'trial', state: 'trial',
remainingMs, remainingMs: trialEndAt - Date.now(),
remainingHours: Math.max(1, Math.ceil(remainingMs / 3600000)), remainingHours: Math.max(1, Math.ceil((trialEndAt - Date.now()) / 3600000)),
trialEndAt: start + TRIAL_MS trialEndAt,
quotaTotal: TRIAL_QUOTA,
quotaUsed: usage.total,
quotaLeft: Math.max(0, TRIAL_QUOTA - usage.total)
}; };
} }
return { state: 'locked' };
// 到期(时间到 或 额度用完)→ 降级体验:不硬锁,保留数据 + 每天 3 条 + 24h 限时优惠
if (!usage.trialEndedAt) {
usage.trialEndedAt = quotaReached && usage.quotaHitAt ? usage.quotaHitAt : trialEndAt;
await saveUsage(usage);
}
const today = todayStr();
const dayCount = usage.day === today ? usage.dayCount : 0;
const dayLeft = Math.max(0, GRACE_DAILY - dayCount);
const promoLeftMs = Math.max(0, usage.trialEndedAt + PROMO.windowMs - Date.now());
return {
state: 'grace',
trialEndedAt: usage.trialEndedAt,
graceDaily: GRACE_DAILY,
dayCount,
dayLeft,
quotaTotal: TRIAL_QUOTA,
quotaUsed: usage.total,
promo: {
code: PROMO.code,
percent: PROMO.percent,
promoLeftMs,
active: promoLeftMs > 0
}
};
}
// 每成功一条评论调用一次:试用期扣 100 条额度;到期后扣每日 3 条;授权用户不计
// 返回 allowed=false 时调用方应立即停止本轮任务
async function usageTick() {
const { licenseState } = await chrome.storage.local.get(['licenseState']);
if (licenseState && licenseState.valid) {
return { allowed: true, state: 'authorized' };
}
const { trialStartedAt } = await chrome.storage.local.get(['trialStartedAt']);
const start = trialStartedAt || Date.now();
const usage = await getUsage();
const inTrial = Date.now() < start + TRIAL_MS && !usage.quotaHitAt;
if (inTrial) {
usage.total += 1;
if (usage.total >= TRIAL_QUOTA && !usage.quotaHitAt) {
usage.quotaHitAt = Date.now();
usage.trialEndedAt = usage.quotaHitAt;
}
await saveUsage(usage);
const quotaLeft = Math.max(0, TRIAL_QUOTA - usage.total);
return {
allowed: quotaLeft > 0,
state: quotaLeft > 0 ? 'trial' : 'grace',
quotaLeft,
message: quotaLeft > 0 ? '' :
`试用额度 ${TRIAL_QUOTA} 条已用完,已自动停止;到期后每天仍可体验 ${GRACE_DAILY} 条`
};
}
// 降级期:每日额度
const today = todayStr();
if (usage.day !== today) {
usage.day = today;
usage.dayCount = 0;
}
usage.dayCount += 1;
usage.total += 1;
await saveUsage(usage);
const dayLeft = Math.max(0, GRACE_DAILY - usage.dayCount);
return {
allowed: dayLeft > 0,
state: 'grace',
dayCount: usage.dayCount,
graceDaily: GRACE_DAILY,
dayLeft,
message: dayLeft > 0 ? '' :
`今日 ${GRACE_DAILY} 条体验额度已用完(明天恢复),购买授权解锁无限量`
};
} }
// 创建购买订单(contact_info 带机器码 → 支付成功后系统自动发卡绑定) // 创建购买订单(contact_info 带机器码 → 支付成功后系统自动发卡绑定)
@@ -362,26 +470,42 @@ async function authCreateOrder(payType) {
const cardType = typesRes.data[0]; const cardType = typesRes.data[0];
const machineId = await getDeviceId(); const machineId = await getDeviceId();
const res = await authFetch('/api/public/orders', { const price = Number(cardType.price);
method: 'POST', // 试用到期 24 小时内下单自动带限时优惠码(服务端按折扣重算实付金额)
body: { const status = await authGetStatus();
const useCoupon = !!(status.state === 'grace' && status.promo && status.promo.active);
const orderBody = () => ({
project_id: AUTH_PROJECT_ID, project_id: AUTH_PROJECT_ID,
card_type_id: cardType.id, card_type_id: cardType.id,
amount: Number(cardType.price), amount: price,
pay_type: pay, pay_type: pay,
contact_info: `MACHINE:${machineId}` contact_info: `MACHINE:${machineId}`,
} ...(useCoupon ? { coupon_code: PROMO.code } : {})
}); });
let res = await authFetch('/api/public/orders', { method: 'POST', body: orderBody() });
let couponApplied = useCoupon;
if (!res.ok && useCoupon) {
// 优惠码异常也不能挡住下单:去掉优惠码重试一次
console.log('优惠码下单失败,改用原价重试:', res.message);
const retryBody = orderBody();
delete retryBody.coupon_code;
res = await authFetch('/api/public/orders', { method: 'POST', body: retryBody });
couponApplied = false;
}
if (!res.ok || !res.data) { if (!res.ok || !res.data) {
return { ok: false, message: res.message || '创建订单失败' }; return { ok: false, message: res.message || '创建订单失败' };
} }
// 记录进行中的订单,便于侧边栏恢复轮询 // 记录进行中的订单,便于侧边栏恢复轮询(金额优先用服务端实付金额)
const serverPay = res.data.pay_amount != null ? Number(res.data.pay_amount) : null;
const fallbackPay = couponApplied ? Math.max(0.01, Math.round(price * (1 - PROMO.percent / 100) * 100) / 100) : price;
const pendingOrder = { const pendingOrder = {
orderNo: res.data.order_no, orderNo: res.data.order_no,
payType: pay, payType: pay,
amount: Number(cardType.price), amount: serverPay != null ? serverPay : fallbackPay,
originalPrice: price,
couponCode: couponApplied ? PROMO.code : '',
packageName: cardType.name || '', packageName: cardType.name || '',
createdAt: Date.now() createdAt: Date.now()
}; };
@@ -451,6 +575,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
return true; return true;
} }
if (request.action === 'usageTick') {
usageTick().then((data) => sendResponse({ ok: true, data }));
return true;
}
if (request.action === 'authActivate') { if (request.action === 'authActivate') {
authVerify(request.cardKey).then((res) => sendResponse(res)); authVerify(request.cardKey).then((res) => sendResponse(res));
return true; return true;
+38 -152
View File
@@ -17,11 +17,9 @@ class XHSNoteSaver {
this.memoryThreshold = 50 * 1024 * 1024; // 内存阈值(50MB) this.memoryThreshold = 50 * 1024 * 1024; // 内存阈值(50MB)
// 评论模式配置 // 评论模式配置
this.commentMode = 'custom'; // 'custom' / 'text' / 'image' / 'ai' this.commentMode = 'custom'; // 'custom' / 'text' / 'ai'
this.customComments = []; // 固定评论池 this.customComments = []; // 固定评论池
this.currentCommentIndex = 0; // 当前评论索引 this.currentCommentIndex = 0; // 当前评论索引
this.currentImageIndex = 0; // 当前图片索引(图片回复模式)
this.imageCommentWarned = false; // 图片失败提示只弹一次
this.init(); this.init();
} }
@@ -81,7 +79,7 @@ class XHSNoteSaver {
// 侧边栏修改评论模式时立即生效(无需刷新页面) // 侧边栏修改评论模式时立即生效(无需刷新页面)
chrome.storage.onChanged.addListener((changes, namespace) => { chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace !== 'local' || !changes.commentMode) return; if (namespace !== 'local' || !changes.commentMode) return;
const mode = changes.commentMode.newValue; const mode = changes.commentMode.newValue === 'image' ? 'custom' : changes.commentMode.newValue;
if (mode && mode !== this.commentMode) { if (mode && mode !== this.commentMode) {
this.commentMode = mode; this.commentMode = mode;
console.log('评论模式已同步:', mode); console.log('评论模式已同步:', mode);
@@ -96,7 +94,7 @@ class XHSNoteSaver {
try { try {
const result = await chrome.storage.local.get(['commentMode']); const result = await chrome.storage.local.get(['commentMode']);
if (result.commentMode) { if (result.commentMode) {
this.commentMode = result.commentMode; this.commentMode = result.commentMode === 'image' ? 'custom' : result.commentMode;
console.log(`评论模式已恢复: ${this.commentMode}`); console.log(`评论模式已恢复: ${this.commentMode}`);
} }
} catch (error) { } catch (error) {
@@ -1600,27 +1598,9 @@ class XHSNoteSaver {
return; return;
} }
// 4. 输入评论(图片模式改为上传图片) // 4. 输入评论
let sentComment = ''; const sentComment = await this.inputComment();
let sentImage = null;
if (this.commentMode === 'image') {
try {
sentImage = await this.uploadCommentImage();
sentComment = '[图片]';
console.log('评论图片已添加');
} catch (error) {
console.error('图片评论失败:', error);
// 只提示一次,避免每篇笔记都弹
if (!this.imageCommentWarned) {
this.imageCommentWarned = true;
this.showToast('⚠️ 图片评论发送失败:' + error.message + ';建议切换「自定义文字」或「AI」模式');
}
return; // 跳过本篇
}
} else {
sentComment = await this.inputComment();
console.log('评论已输入'); console.log('评论已输入');
}
// 检查是否被停止 // 检查是否被停止
if (!this.isAutoClicking) { if (!this.isAutoClicking) {
@@ -1633,9 +1613,11 @@ class XHSNoteSaver {
console.log('评论已提交'); console.log('评论已提交');
// 6. 校验是否发送成功并记录 // 6. 校验是否发送成功并记录
const posted = await this.verifyCommentPosted(sentComment, this.commentMode === 'image', sentImage); const posted = await this.verifyCommentPosted(sentComment);
if (posted) { if (posted) {
await this.logCommentSuccess(sentComment, this.commentMode === 'image', sentImage); await this.logCommentSuccess(sentComment);
// 试用额度计数(授权用户不计);额度用完自动停止
await this.checkUsageQuota();
} else { } else {
console.warn('评论可能未发送成功,不计入记录'); console.warn('评论可能未发送成功,不计入记录');
this.showToast('⚠️ 评论可能未发送成功,本篇未记录'); this.showToast('⚠️ 评论可能未发送成功,本篇未记录');
@@ -1860,107 +1842,17 @@ class XHSNoteSaver {
const labels = { const labels = {
custom: '固定评论池', custom: '固定评论池',
text: '自定义文字', text: '自定义文字',
image: '图片回复',
ai: 'AI按笔记生成' ai: 'AI按笔记生成'
}; };
return labels[mode] || mode; return labels[mode] || mode;
} }
// 上传评论图片(image模式,多张图片循环发送)
async uploadCommentImage() {
const result = await chrome.storage.local.get(['commentImages']);
const images = result.commentImages || [];
if (images.length === 0) {
throw new Error('尚未在侧边栏上传评论图片');
}
const image = images[this.currentImageIndex % images.length];
this.currentImageIndex++;
console.log(`使用评论图片 (${this.currentImageIndex}/${images.length}):`, image.name || image.id);
// dataURL 转 File
const blob = await (await fetch(image.dataUrl)).blob();
const file = new File([blob], image.name || `comment-${Date.now()}.jpg`, {
type: blob.type || 'image/jpeg'
});
// 查找评论区的图片上传入口
let fileInput = this.findCommentFileInput();
if (!fileInput) {
fileInput = await this.revealCommentFileInput();
}
if (!fileInput) {
throw new Error('网页版评论框没有图片上传入口(小红书仅手机App支持图片评论)');
}
// 注入文件并触发 change 事件
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
// 等待图片预览出现
await this.sleep(1500);
console.log('评论图片已注入');
return image;
}
// 在评论区查找文件上传 input
findCommentFileInput() {
const engageBar = document.querySelector('#noteContainer .engage-bar') || document.querySelector('.engage-bar');
if (engageBar) {
const scoped = engageBar.querySelector('input[type="file"]');
if (scoped) return scoped;
}
return document.querySelector('#noteContainer input[type="file"]')
|| document.querySelector('input[type="file"][accept*="image"]');
}
// 点击图片图标按钮,尝试唤出隐藏的 file input
async revealCommentFileInput() {
const engageBar = document.querySelector('#noteContainer .engage-bar') || document.querySelector('.engage-bar');
if (!engageBar) return null;
const candidates = engageBar.querySelectorAll(
'[class*="image"], [class*="img"], [class*="picture"], [class*="upload"], svg[class*="image"], svg[class*="img"]'
);
console.log(`尝试 ${candidates.length} 个候选图片按钮`);
for (const candidate of candidates) {
try {
// 点击图标外层可点击容器(通常 icon 的父级才有事件)
const clickable = candidate.closest('span, div, button, [role="button"]') || candidate;
clickable.click();
await this.sleep(600);
const input = this.findCommentFileInput();
if (input) {
console.log('已唤出文件上传入口:', clickable.className || clickable.tagName);
return input;
}
} catch (error) {
console.log('点击图片按钮失败:', error);
}
}
return null;
}
// 校验评论是否发送成功(提交后延迟检查) // 校验评论是否发送成功(提交后延迟检查)
async verifyCommentPosted(commentText, isImage) { async verifyCommentPosted(commentText) {
await this.sleep(1500); await this.sleep(1500);
try { 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 textarea = document.querySelector('#content-textarea');
const textareaEmpty = !textarea || !(textarea.textContent || '').trim(); const textareaEmpty = !textarea || !(textarea.textContent || '').trim();
@@ -1975,8 +1867,31 @@ class XHSNoteSaver {
} }
} }
// 试用额度:每成功一条让后台计数;allowed=false 时立即停止本轮任务
async checkUsageQuota() {
try {
const res = await chrome.runtime.sendMessage({ action: 'usageTick' });
const d = res && res.data;
if (!d) return;
if (d.allowed === false) {
this.isAutoClicking = false;
this.cleanup();
this.showToast('⛔ ' + (d.message || '体验额度已用完,已自动停止'));
console.log('额度用完,已停止:', d);
return;
}
if (d.state === 'trial' && typeof d.quotaLeft === 'number' && d.quotaLeft > 0 && d.quotaLeft <= 10) {
this.showToast(`试用额度还剩 ${d.quotaLeft} 条`);
} else if (d.state === 'grace' && typeof d.dayLeft === 'number' && d.dayLeft > 0) {
console.log(`体验模式:今日剩余 ${d.dayLeft} 条`);
}
} catch (error) {
console.log('额度检查失败(不阻断):', error);
}
}
// 记录一条成功的评论(写入 storage,侧边栏展示统计与列表) // 记录一条成功的评论(写入 storage,侧边栏展示统计与列表)
async logCommentSuccess(commentText, isImage = false, image = null) { async logCommentSuccess(commentText) {
try { try {
let url = ''; let url = '';
if (this.currentNoteData && this.currentNoteData.url) { if (this.currentNoteData && this.currentNoteData.url) {
@@ -2001,20 +1916,10 @@ class XHSNoteSaver {
url, url,
title: title.slice(0, 100), title: title.slice(0, 100),
comment: (commentText || '').slice(0, 200), comment: (commentText || '').slice(0, 200),
isImage: !!isImage,
mode: this.commentMode, mode: this.commentMode,
timestamp: Date.now() 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 result = await chrome.storage.local.get(['commentLogs']);
const logs = result.commentLogs || []; const logs = result.commentLogs || [];
logs.unshift(record); logs.unshift(record);
@@ -2026,25 +1931,6 @@ class XHSNoteSaver {
} }
} }
// 生成小尺寸缩略图(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评论(简化版,避免重复代码) // 生成AI评论(简化版,避免重复代码)
async generateAiComment() { async generateAiComment() {
try { try {
@@ -2359,9 +2245,9 @@ class XHSNoteSaver {
console.log('========================'); console.log('========================');
} }
// 切换评论模式(固定评论池 → 自定义文字 → 图片回复 → AI → 循环) // 切换评论模式(固定评论池 → 自定义文字 → AI → 循环)
async toggleCommentMode() { async toggleCommentMode() {
const modes = ['custom', 'text', 'image', 'ai']; const modes = ['custom', 'text', 'ai'];
const currentIndex = modes.indexOf(this.commentMode); const currentIndex = modes.indexOf(this.commentMode);
this.commentMode = modes[(currentIndex + 1) % modes.length]; this.commentMode = modes[(currentIndex + 1) % modes.length];
console.log(`切换评论模式为: ${this.commentMode}`); console.log(`切换评论模式为: ${this.commentMode}`);
@@ -2382,7 +2268,7 @@ class XHSNoteSaver {
updateCommentModeButton() { updateCommentModeButton() {
const commentModeBtn = document.getElementById('xhs-comment-mode'); const commentModeBtn = document.getElementById('xhs-comment-mode');
if (commentModeBtn) { if (commentModeBtn) {
const icons = { custom: '📋', text: '📝', image: '🖼️', ai: '🤖' }; const icons = { custom: '📋', text: '📝', ai: '🤖' };
commentModeBtn.textContent = `${icons[this.commentMode] || '💬'} ${this.getCommentModeLabel()}`; commentModeBtn.textContent = `${icons[this.commentMode] || '💬'} ${this.getCommentModeLabel()}`;
if (this.commentMode === 'ai') { if (this.commentMode === 'ai') {
commentModeBtn.classList.remove('xhs-secondary-btn'); commentModeBtn.classList.remove('xhs-secondary-btn');
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "小红书自动评论助手", "name": "小红书自动评论助手",
"version": "1.7.7", "version": "1.7.9",
"description": "右侧侧边栏常驻的自动评论助手,支持试用/卡密授权、固定池/自定义文字/图片/AI评论", "description": "右侧侧边栏常驻的自动评论助手,支持试用/卡密授权、固定池/自定义文字/图片/AI评论",
"permissions": [ "permissions": [
"storage", "storage",
+3 -4
View File
@@ -16,7 +16,7 @@
</head> </head>
<body> <body>
<h1>小红书自动评论助手 · 隐私政策</h1> <h1>小红书自动评论助手 · 隐私政策</h1>
<div class="meta">生效日期:2026年10月10日 · 适用版本:v1.7.7 及以上</div> <div class="meta">生效日期:2026年10月10日 · 适用版本:v1.7.9 及以上</div>
<p>感谢使用「小红书自动评论助手」浏览器扩展(以下简称"本插件")。我们重视你的隐私。本页面说明本插件收集哪些信息、如何使用以及如何保护。</p> <p>感谢使用「小红书自动评论助手」浏览器扩展(以下简称"本插件")。我们重视你的隐私。本页面说明本插件收集哪些信息、如何使用以及如何保护。</p>
@@ -32,7 +32,6 @@
以下数据仅保存在你自己的浏览器中(chrome.storage.local),<b>不会上传到任何服务器</b>: 以下数据仅保存在你自己的浏览器中(chrome.storage.local),<b>不会上传到任何服务器</b>:
<ul style="margin-top:8px"> <ul style="margin-top:8px">
<li>自定义评论文字库</li> <li>自定义评论文字库</li>
<li>上传的评论图片</li>
<li>评论记录(笔记标题、评论内容、时间)</li> <li>评论记录(笔记标题、评论内容、时间)</li>
<li>AI 模型配置(API 地址、模型名、密钥)</li> <li>AI 模型配置(API 地址、模型名、密钥)</li>
<li>搜索地区、评论方式等偏好设置</li> <li>搜索地区、评论方式等偏好设置</li>
@@ -44,7 +43,7 @@
<h2>四、信息的使用目的</h2> <h2>四、信息的使用目的</h2>
<ul> <ul>
<li>授权验证与到期管理(含1天试用的计时)</li> <li>授权验证与到期管理(含 3 天试用、100 条额度与到期体验额度的计时)</li>
<li>安装统计与活跃统计(帮助我们了解插件覆盖情况)</li> <li>安装统计与活跃统计(帮助我们了解插件覆盖情况)</li>
<li>向你推送官方公告(可选,每条公告每设备最多弹出一次)</li> <li>向你推送官方公告(可选,每条公告每设备最多弹出一次)</li>
<li>防止授权被多设备滥用</li> <li>防止授权被多设备滥用</li>
@@ -57,7 +56,7 @@
<ul> <ul>
<li>设备标识为随机生成,无法反推出你的任何个人身份信息</li> <li>设备标识为随机生成,无法反推出你的任何个人身份信息</li>
<li>授权服务器仅记录设备标识、授权状态与时间,不记录你评论了哪篇笔记</li> <li>授权服务器仅记录设备标识、授权状态与时间,不记录你评论了哪篇笔记</li>
<li>评论内容与图片等敏感数据仅存于你的浏览器本地</li> <li>评论内容等敏感数据仅存于你的浏览器本地</li>
</ul> </ul>
<h2>七、你的权利</h2> <h2>七、你的权利</h2>
+38 -79
View File
@@ -556,31 +556,10 @@ body {
/* 自定义文字库面板 */ /* 自定义文字库面板 */
.text-comment-config, .text-comment-config,
.image-comment-config {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.text-comment-config label, .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, .text-comment-config small,
.image-comment-config small {
display: block;
margin-top: 6px;
color: #666;
font-size: 11px;
line-height: 1.4;
}
.panel-count { .panel-count {
font-size: 12px; font-size: 12px;
@@ -588,64 +567,6 @@ body {
align-self: center; 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 引导横幅 */ /* 免费获取模型 KEY 引导横幅 */
.ai-key-guide { .ai-key-guide {
@@ -879,6 +800,44 @@ body {
color: #c62828; color: #c62828;
} }
.auth-badge.grace {
background: #fff8e1;
color: #f57f17;
border: 1px solid #ffca28;
}
.coupon-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
padding: 8px 10px;
border: 1px dashed #ffab40;
background: #fff8e1;
border-radius: 8px;
font-size: 12px;
color: #7a4b00;
line-height: 1.5;
}
.coupon-bar .coupon-text { flex: 1; min-width: 0; }
.coupon-bar b { color: #e65100; }
.coupon-copy-btn {
flex-shrink: 0;
border: none;
background: #ff9800;
color: #fff;
font-size: 12px;
font-weight: 600;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
}
.coupon-copy-btn:active { transform: scale(.96); }
.auth-detail { .auth-detail {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
+5 -12
View File
@@ -22,6 +22,10 @@
<button id="activate-btn" class="activate-btn">激活</button> <button id="activate-btn" class="activate-btn">激活</button>
<button id="buy-btn" class="buy-btn">购买授权</button> <button id="buy-btn" class="buy-btn">购买授权</button>
</div> </div>
<div class="coupon-bar" id="coupon-bar" hidden>
<span class="coupon-text">⏰ 试用到期限时优惠:码 <b id="coupon-code">XHS24H</b> · <span id="coupon-percent">8 折</span> · 剩余 <b id="coupon-left">--:--:--</b></span>
<button type="button" id="coupon-copy-btn" class="coupon-copy-btn">复制</button>
</div>
<div class="pay-select-row" id="pay-select-row" hidden> <div class="pay-select-row" id="pay-select-row" hidden>
<span class="pay-select-label">选择支付方式:</span> <span class="pay-select-label">选择支付方式:</span>
<button class="pay-btn" data-pay="alipay">支付宝</button> <button class="pay-btn" data-pay="alipay">支付宝</button>
@@ -105,10 +109,6 @@
<input type="radio" name="comment-mode" id="mode-text" value="text"> <input type="radio" name="comment-mode" id="mode-text" value="text">
<span>自定义文字</span> <span>自定义文字</span>
</label> </label>
<label class="mode-option" for="mode-image">
<input type="radio" name="comment-mode" id="mode-image" value="image">
<span>图片回复</span>
</label>
<label class="mode-option" for="mode-ai"> <label class="mode-option" for="mode-ai">
<input type="radio" name="comment-mode" id="mode-ai" value="ai"> <input type="radio" name="comment-mode" id="mode-ai" value="ai">
<span>AI 按笔记生成</span> <span>AI 按笔记生成</span>
@@ -125,12 +125,6 @@
</div> </div>
<small>改动即时保存已打开的小红书页面;一行一条,发完最后一条从头开始循环</small> <small>改动即时保存已打开的小红书页面;一行一条,发完最后一条从头开始循环</small>
</div> </div>
<div class="image-comment-config" id="image-comment-panel">
<label for="comment-image-input">图片回复(可选多张,按顺序循环发送)</label>
<input type="file" id="comment-image-input" class="image-file-input" accept="image/*" multiple />
<div id="image-preview-list" class="image-preview-list"></div>
<small>图片会压缩后保存在本地(浏览器存储),大图建议先自行压缩;图片发完最后一条从头开始循环</small>
</div>
<a class="ai-key-guide" href="https://ai.88531.cn" target="_blank" rel="noopener" title="点击前往 ai.88531.cn 免费获取模型 KEY"> <a class="ai-key-guide" href="https://ai.88531.cn" target="_blank" rel="noopener" title="点击前往 ai.88531.cn 免费获取模型 KEY">
🔑 还没有模型KEY?<span class="ai-key-guide-cta">免费获取模型KEY</span> → ai.88531.cn 🔑 还没有模型KEY?<span class="ai-key-guide-cta">免费获取模型KEY</span> → ai.88531.cn
</a> </a>
@@ -195,7 +189,6 @@
<li>🧪 <strong>元素检测</strong>:测试页面元素是否被正确识别</li> <li>🧪 <strong>元素检测</strong>:测试页面元素是否被正确识别</li>
<li>💬 <strong>评论配置</strong>:自定义模型生成评论内容</li> <li>💬 <strong>评论配置</strong>:自定义模型生成评论内容</li>
<li>📝 <strong>自定义文字</strong>:自己写文字库,循环发送</li> <li>📝 <strong>自定义文字</strong>:自己写文字库,循环发送</li>
<li>🖼️ <strong>图片回复</strong>:上传图片,循环以图片评论</li>
<li>📊 <strong>评论记录</strong>:今日/本月/总计统计,点击记录直达笔记</li> <li>📊 <strong>评论记录</strong>:今日/本月/总计统计,点击记录直达笔记</li>
</ul> </ul>
@@ -211,7 +204,7 @@
<div class="footer"> <div class="footer">
<div class="version-info"> <div class="version-info">
<span>版本: 1.7.7</span> <span>版本: 1.7.9</span>
<span>侧边栏常驻 · 专注于自动评论</span> <span>侧边栏常驻 · 专注于自动评论</span>
</div> </div>
</div> </div>
+105 -153
View File
@@ -16,10 +16,9 @@ class AutoCommentManager {
await this.loadRegion(); // 加载按地区搜索设置 await this.loadRegion(); // 加载按地区搜索设置
await this.loadApiKey(); // 加载API密钥 await this.loadApiKey(); // 加载API密钥
await this.loadModelConfig(); // 加载自定义模型配置 await this.loadModelConfig(); // 加载自定义模型配置
await this.loadCommentMode(); // 加载评论方式(固定评论池 / 自定义文字 / 图片 / AI) await this.loadCommentMode(); // 加载评论方式(固定评论池 / 自定义文字 / AI)
await this.loadAiPrompt(); // 加载AI提示词 await this.loadAiPrompt(); // 加载AI提示词
await this.loadUserComments(); // 加载自定义文字库 await this.loadUserComments(); // 加载自定义文字库
await this.loadImagePreviews(); // 加载已上传的评论图片
this.bindEvents(); this.bindEvents();
this.updateAiStatus(); this.updateAiStatus();
this.loadCommentLogs(); // 加载评论记录 this.loadCommentLogs(); // 加载评论记录
@@ -112,15 +111,6 @@ class AutoCommentManager {
}); });
} }
// 图片回复:选择图片即压缩保存
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'); const refreshLogBtn = document.getElementById('refresh-log-btn');
if (refreshLogBtn) { if (refreshLogBtn) {
@@ -537,12 +527,16 @@ class AutoCommentManager {
} }
} }
// 加载评论方式(custom / text / image / ai) // 加载评论方式(custom / text / ai)
async loadCommentMode() { async loadCommentMode() {
try { try {
const result = await chrome.storage.local.get(['commentMode']); const result = await chrome.storage.local.get(['commentMode']);
const validModes = ['custom', 'text', 'image', 'ai']; const validModes = ['custom', 'text', 'ai'];
const mode = validModes.includes(result.commentMode) ? result.commentMode : 'custom'; const mode = validModes.includes(result.commentMode) ? result.commentMode : 'custom';
if (result.commentMode && result.commentMode !== mode) {
// 旧版本残留的 image 模式已移除,回退到固定评论池并写回
await chrome.storage.local.set({ commentMode: mode });
}
const radio = document.getElementById(`mode-${mode}`); const radio = document.getElementById(`mode-${mode}`);
if (radio) radio.checked = true; if (radio) radio.checked = true;
this.updateModePanels(mode); this.updateModePanels(mode);
@@ -567,9 +561,7 @@ class AutoCommentManager {
// 根据评论方式显示/隐藏对应的配置面板 // 根据评论方式显示/隐藏对应的配置面板
updateModePanels(mode) { updateModePanels(mode) {
const textPanel = document.getElementById('text-comment-panel'); const textPanel = document.getElementById('text-comment-panel');
const imagePanel = document.getElementById('image-comment-panel');
if (textPanel) textPanel.style.display = mode === 'text' ? 'block' : 'none'; if (textPanel) textPanel.style.display = mode === 'text' ? 'block' : 'none';
if (imagePanel) imagePanel.style.display = mode === 'image' ? 'block' : 'none';
} }
// 加载自定义文字库 // 加载自定义文字库
@@ -614,133 +606,21 @@ class AutoCommentManager {
if (el) el.textContent = count > 0 ? `共 ${count} 条` : '暂无内容'; if (el) el.textContent = count > 0 ? `共 ${count} 条` : '暂无内容';
} }
// 处理选择的图片文件(压缩后保存到本地存储)
async handleImageFiles(fileList) {
const files = Array.from(fileList || []).filter((f) => f.type.startsWith('image/'));
if (files.length === 0) return;
try {
this.showActionStatus(true, `⏳ 正在处理 ${files.length} 张图片…`);
const result = await chrome.storage.local.get(['commentImages']);
const images = result.commentImages || [];
for (const file of files) {
const dataUrl = await this.compressImage(file);
images.push({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.name,
dataUrl
});
}
await chrome.storage.local.set({ commentImages: images });
this.renderImagePreviews(images);
this.showActionStatus(true, `✓ 已添加 ${files.length} 张图片,共 ${images.length} 张`);
await this.updateModeHint('image');
} catch (error) {
console.error('处理评论图片失败:', error);
this.showActionStatus(false, '✗ 图片处理失败: ' + error.message);
}
}
// 压缩图片:超过 400KB 或边长超过 1280px 时压缩为 JPEG
async compressImage(file) {
const shouldCompress = file.size > 400 * 1024;
const originalDataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
if (!shouldCompress) return originalDataUrl;
const img = await new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = originalDataUrl;
});
const maxSide = 1280;
let { width, height } = img;
if (width > maxSide || height > maxSide) {
const scale = Math.min(maxSide / width, maxSide / height);
width = Math.round(width * scale);
height = Math.round(height * scale);
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
return canvas.toDataURL('image/jpeg', 0.85);
}
// 渲染图片预览列表
async loadImagePreviews() {
try {
const result = await chrome.storage.local.get(['commentImages']);
this.renderImagePreviews(result.commentImages || []);
} catch (error) {
console.error('加载评论图片失败:', error);
}
}
renderImagePreviews(images) {
const list = document.getElementById('image-preview-list');
if (!list) return;
if (!images || images.length === 0) {
list.innerHTML = '<div class="image-preview-empty">暂无图片</div>';
return;
}
list.innerHTML = '';
images.forEach((image) => {
const item = document.createElement('div');
item.className = 'image-preview-item';
const img = document.createElement('img');
img.src = image.dataUrl;
img.alt = image.name || '';
const del = document.createElement('button');
del.type = 'button';
del.className = 'image-delete-btn';
del.textContent = '×';
del.title = '删除这张图片';
del.addEventListener('click', () => this.deleteCommentImage(image.id));
item.appendChild(img);
item.appendChild(del);
list.appendChild(item);
});
}
// 删除一张评论图片
async deleteCommentImage(id) {
try {
const result = await chrome.storage.local.get(['commentImages']);
const images = (result.commentImages || []).filter((img) => img.id !== id);
await chrome.storage.local.set({ commentImages: images });
this.renderImagePreviews(images);
this.showActionStatus(true, `✓ 已删除,剩余 ${images.length} 张`);
await this.updateModeHint('image');
} catch (error) {
console.error('删除评论图片失败:', error);
}
}
// ===== 授权系统(CardAuth) ===== // ===== 授权系统(CardAuth) =====
// 功能门槛:有效授权 或 试用期内可用;试用结束锁定 // 功能门槛:授权 / 试用期 / 到期降级(每日 3 条)都可开始;降级额度用完才拦
async isFeatureAllowed() { async isFeatureAllowed() {
try { try {
const status = await chrome.runtime.sendMessage({ action: 'authStatus' }); const status = await chrome.runtime.sendMessage({ action: 'authStatus' });
const state = status && status.status ? status.status.state : 'locked'; const s = status && status.status ? status.status : null;
if (state === 'locked') { if (!s) return true; // 拿不到状态时放行,不锁死
this.showActionStatus(false, '✗ 试用已结束,请购买授权或输入卡密激活'); if (s.state === 'authorized' || s.state === 'trial') return true;
if (s.state === 'grace') {
if (s.dayLeft > 0) {
this.showActionStatus(true, `体验模式:今日还剩 ${s.dayLeft}/${s.graceDaily} 条,用完明天恢复`);
return true;
}
this.showActionStatus(false, `✗ 今日 ${s.graceDaily} 条体验额度已用完(明天恢复),购买授权解锁无限量`);
return false; return false;
} }
return true; return true;
@@ -756,10 +636,31 @@ class AutoCommentManager {
await this.renderAuthStatus(); await this.renderAuthStatus();
await this.loadAnnouncements(); await this.loadAnnouncements();
// 优惠码复制
const copyBtn = document.getElementById('coupon-copy-btn');
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
const codeEl = document.getElementById('coupon-code');
const code = (codeEl ? codeEl.textContent : '').trim();
if (!code) return;
try {
await navigator.clipboard.writeText(code);
} catch (error) {
const ta = document.createElement('textarea');
ta.value = code;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
this.showActionStatus(true, `✓ 优惠码 ${code} 已复制,点「购买授权」下单自动抵扣`);
});
}
// 后台复验或购买激活后自动刷新 // 后台复验或购买激活后自动刷新
chrome.storage.onChanged.addListener((changes, namespace) => { chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace !== 'local') return; if (namespace !== 'local') return;
if (changes.licenseState) this.renderAuthStatus(); if (changes.licenseState || changes.trialUsage) this.renderAuthStatus();
if (changes.announcements) this.renderAnnouncements((changes.announcements.newValue || {}).list || []); if (changes.announcements) this.renderAnnouncements((changes.announcements.newValue || {}).list || []);
}); });
} }
@@ -774,7 +675,7 @@ class AutoCommentManager {
const status = await chrome.runtime.sendMessage({ action: 'authStatus' }); const status = await chrome.runtime.sendMessage({ action: 'authStatus' });
const s = status && status.status ? status.status : { state: 'locked' }; const s = status && status.status ? status.status : { state: 'locked' };
badge.classList.remove('authorized', 'trial', 'locked'); badge.classList.remove('authorized', 'trial', 'locked', 'grace');
this.stopTrialCountdown(); this.stopTrialCountdown();
if (s.state === 'authorized') { if (s.state === 'authorized') {
@@ -789,14 +690,23 @@ class AutoCommentManager {
} else { } else {
detail.textContent = '授权有效'; detail.textContent = '授权有效';
} }
this.hideCouponBar();
} else if (s.state === 'trial') { } else if (s.state === 'trial') {
badge.classList.add('trial'); badge.classList.add('trial');
badge.textContent = '试用中'; badge.textContent = '试用中';
this.startTrialCountdown(s.trialEndAt || (Date.now() + 3600000)); this.hideCouponBar();
this.startTrialCountdown(s.trialEndAt || (Date.now() + 3600000), s);
} else if (s.state === 'grace') {
badge.classList.add('grace');
badge.textContent = '体验模式';
detail.textContent = `试用已到期 · 今日剩 ${s.dayLeft}/${s.graceDaily} 条(每天恢复)` +
(s.promo && s.promo.active ? ` · 限时优惠码 ${s.promo.code}` : '');
this.renderCouponBar(s.promo);
} else { } else {
badge.classList.add('locked'); badge.classList.add('locked');
badge.textContent = '未授权'; badge.textContent = '未授权';
detail.textContent = '试用已结束:请购买授权或输入卡密激活后使用'; detail.textContent = '授权状态异常,请重新打开侧边栏';
this.hideCouponBar();
} }
} catch (error) { } catch (error) {
console.error('获取授权状态失败:', error); console.error('获取授权状态失败:', error);
@@ -807,8 +717,9 @@ class AutoCommentManager {
} }
// 试用倒计时(精确到秒,每秒刷新) // 试用倒计时(精确到秒,每秒刷新)
startTrialCountdown(endAt) { startTrialCountdown(endAt, status) {
this.stopTrialCountdown(); this.stopTrialCountdown();
const quotaLeft = status && status.quotaLeft != null ? status.quotaLeft : null;
const tick = () => { const tick = () => {
const remain = endAt - Date.now(); const remain = endAt - Date.now();
@@ -816,12 +727,13 @@ class AutoCommentManager {
if (remain <= 0) { if (remain <= 0) {
this.stopTrialCountdown(); this.stopTrialCountdown();
this.renderAuthStatus(); // 到期后刷新为锁定状态 this.renderAuthStatus(); // 到期后刷新为降级体验状态
return; return;
} }
if (detail) { if (detail) {
detail.textContent = `免费试用剩余 ${this.formatRemain(remain)} · 试用结束后需购买授权或输入卡密激活`; const quotaText = quotaLeft != null ? ` · 额度剩 ${quotaLeft} 条` : '';
detail.textContent = `免费试用剩余 ${this.formatRemain(remain)}${quotaText} · 到期后每天仍可体验 3 条`;
} }
}; };
@@ -836,6 +748,50 @@ class AutoCommentManager {
} }
} }
// ===== 到期 24 小时限时优惠条 =====
renderCouponBar(promo) {
const bar = document.getElementById('coupon-bar');
if (!bar) return;
if (!promo || !promo.active) {
this.hideCouponBar();
return;
}
const codeEl = document.getElementById('coupon-code');
const pctEl = document.getElementById('coupon-percent');
if (codeEl) codeEl.textContent = promo.code;
if (pctEl) pctEl.textContent = `${100 - promo.percent} 折(立减 ${promo.percent}%)`;
bar.hidden = false;
this.promoFetchedAt = Date.now();
this.promoLeftMs = promo.promoLeftMs;
this.stopCouponTimer();
const tick = () => {
const left = this.promoLeftMs - (Date.now() - this.promoFetchedAt);
const leftEl = document.getElementById('coupon-left');
if (left <= 0) {
this.hideCouponBar();
this.renderAuthStatus();
return;
}
if (leftEl) leftEl.textContent = this.formatRemain(left);
};
tick();
this.couponTimer = setInterval(tick, 1000);
}
hideCouponBar() {
const bar = document.getElementById('coupon-bar');
if (bar) bar.hidden = true;
this.stopCouponTimer();
}
stopCouponTimer() {
if (this.couponTimer) {
clearInterval(this.couponTimer);
this.couponTimer = null;
}
}
// 毫秒 → HH:MM:SS // 毫秒 → HH:MM:SS
formatRemain(ms) { formatRemain(ms) {
const totalSec = Math.max(0, Math.floor(ms / 1000)); const totalSec = Math.max(0, Math.floor(ms / 1000));
@@ -893,7 +849,10 @@ class AutoCommentManager {
} }
const order = res.order; const order = res.order;
this.showOrderStatus(true, `订单 ${order.orderNo}(¥${order.amount})已创建,请在新页面完成支付,支付成功后自动激活…`); const priceText = order.couponCode
? `¥${order.amount},原价 ¥${order.originalPrice},已用优惠码 ${order.couponCode}`
: `¥${order.amount}`;
this.showOrderStatus(true, `订单 ${order.orderNo}(${priceText})已创建,请在新页面完成支付,支付成功后自动激活…`);
if (res.payUrl) { if (res.payUrl) {
chrome.tabs.create({ url: res.payUrl }); chrome.tabs.create({ url: res.payUrl });
} }
@@ -1139,12 +1098,11 @@ class AutoCommentManager {
let storageInfo = {}; let storageInfo = {};
try { try {
storageInfo = await chrome.storage.local.get(['userComments', 'commentImages']); storageInfo = await chrome.storage.local.get(['userComments']);
} catch (error) { } catch (error) {
console.error('读取提示信息失败:', error); console.error('读取提示信息失败:', error);
} }
const textCount = (storageInfo.userComments || []).length; const textCount = (storageInfo.userComments || []).length;
const imageCount = (storageInfo.commentImages || []).length;
hint.classList.remove('warn'); hint.classList.remove('warn');
@@ -1154,9 +1112,6 @@ class AutoCommentManager {
} else if (current === 'text' && textCount === 0) { } else if (current === 'text' && textCount === 0) {
hint.textContent = '⚠ 自定义文字模式还没有文字内容,请在下方填写并保存'; hint.textContent = '⚠ 自定义文字模式还没有文字内容,请在下方填写并保存';
hint.classList.add('warn'); hint.classList.add('warn');
} else if (current === 'image' && imageCount === 0) {
hint.textContent = '⚠ 小红书网页版评论不支持发图片(仅手机App支持),此模式大概率发不出,建议用「自定义文字」或「AI」';
hint.classList.add('warn');
} else if (current === 'custom' && hasKey) { } else if (current === 'custom' && hasKey) {
hint.textContent = '⚠ 当前是固定评论池,下方模型配置不会生效;要按笔记生成请点「AI 按笔记生成」'; hint.textContent = '⚠ 当前是固定评论池,下方模型配置不会生效;要按笔记生成请点「AI 按笔记生成」';
hint.classList.add('warn'); hint.classList.add('warn');
@@ -1164,9 +1119,6 @@ class AutoCommentManager {
hint.textContent = '✓ 每篇笔记都会调用下方模型,按笔记内容生成评论'; hint.textContent = '✓ 每篇笔记都会调用下方模型,按笔记内容生成评论';
} else if (current === 'text') { } else if (current === 'text') {
hint.textContent = `✓ 每篇笔记发送一条自定义文字(共 ${textCount} 条循环)`; hint.textContent = `✓ 每篇笔记发送一条自定义文字(共 ${textCount} 条循环)`;
} else if (current === 'image') {
hint.textContent = `⚠ 已上传 ${imageCount} 张,但小红书网页版评论不支持发图片(仅手机App支持),大概率发送失败,建议改用「自定义文字」或「AI」`;
hint.classList.add('warn');
} else { } else {
hint.textContent = '固定评论池 = 循环发预设短评;下方模型配置暂不生效'; hint.textContent = '固定评论池 = 循环发预设短评;下方模型配置暂不生效';
} }