feat: v1.7.9 试用72h+100条额度+到期降级+24h优惠码;v1.7.8 移除图片回复模式
注:本提交由 Gitee v1.7.7 基线重建(原 GitHub 仓库账号暂停、源码包已丢),重建后经真机功能测试与 v1.7.9 行为一致。
This commit is contained in:
1 parent
fae84ff6c7
commit
540f506c78
8 files changed
+355
-431
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 一键搜索 · 自动点击 · 智能评论 —— 小红书运营引流的效率神器
|
> 一键搜索 · 自动点击 · 智能评论 —— 小红书运营引流的效率神器
|
||||||
|
|
||||||
[](https://dl.88531.cn/xhs.html)
|
[](https://dl.88531.cn/xhs.html)
|
||||||
[](#安装方法)
|
[](#安装方法)
|
||||||
[](#开发说明)
|
[](#开发说明)
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = '固定评论池 = 循环发预设短评;下方模型配置暂不生效';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user