feat: 小红书自动评论助手 v1.7.5
- 侧边栏常驻:搜索/自动点击/自动评论全流程 - 4种评论方式:固定池/自定义文字/图片回复/AI按笔记生成 - 按地区搜索、评论记录统计(今日/本月,点击直达笔记) - CardAuth 授权:1天试用倒计时、卡密激活、购买自动发卡 - 公告推送、安装统计、小红书风格图标
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# 📕 小红书自动评论助手
|
||||||
|
|
||||||
|
> 一键搜索 · 自动点击 · 智能评论 —— 小红书运营引流的效率神器
|
||||||
|
|
||||||
|
[](https://dl.88531.cn/xhs.html)
|
||||||
|
[](#安装方法)
|
||||||
|
[](#开发说明)
|
||||||
|
|
||||||
|
一个专注于自动评论功能的浏览器插件:在浏览器**右侧侧边栏常驻**,输入关键词即可自动搜索小红书、逐篇点开笔记、发表评论,全程无需人工干预。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 为什么选择我们
|
||||||
|
|
||||||
|
| 亮点 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 🚀 **全流程自动化** | 输入关键词 → 自动开搜 → 自动点开每篇笔记 → 自动写评论 → 自动提交,一条龙跑完 |
|
||||||
|
| 🤖 **4 种评论方式** | 固定评论池 / 自定义文字库 / **图片回复** / **AI 按笔记生成**,随心切换 |
|
||||||
|
| 🧠 **AI 智能评论** | 接入任意 OpenAI 兼容接口,每篇笔记结合内容生成不同评论,告别复读机 |
|
||||||
|
| 🖼️ **图片评论** | 上传表情包/图片,每篇笔记自动发一张,多张循环,互动更自然 |
|
||||||
|
| 📍 **按地区搜索** | 选「上海」「杭州西湖区」自动拼进关键词,精准触达同城流量 |
|
||||||
|
| 📊 **评论数据统计** | 今日/本月/总计成功数一目了然,每条记录带标题+评论内容,**点击直达笔记** |
|
||||||
|
| 🔐 **免费试用 1 天** | 装完即用,满意再买;插件内支付宝/微信购买,**支付成功自动发卡自动激活** |
|
||||||
|
| 📢 **公告推送** | 官方公告实时下发(弹窗/列表),运营消息不错过 |
|
||||||
|
| 🛡️ **稳定可靠** | 元素检测+备用方案、失败自动重试、内存自动防护、页面结构变化可快速适配 |
|
||||||
|
| 🔒 **数据本地存储** | 评论库、图片、记录全部存在你自己的浏览器里,不上传第三方 |
|
||||||
|
|
||||||
|
## 📦 功能总览
|
||||||
|
|
||||||
|
- **🔍 智能搜索**:关键词搜索 + 地区筛选(18 个主要城市 + 自定义到区/商圈级)
|
||||||
|
- **🤖 自动点击**:精确定位笔记卡片链接,支持「展开内容」或「简单点击」两种模式,自动滚动加载更多
|
||||||
|
- **💬 评论方式**:
|
||||||
|
- 固定评论池:循环发预设短评(👍 赞!/💯 太棒了!…)
|
||||||
|
- 自定义文字:侧边栏文字库一行一条,循环发送
|
||||||
|
- 图片回复:上传图片自动压缩存本地,预览/删除管理
|
||||||
|
- AI 按笔记生成:提示词可自定义(`{{content}}` 占位符),失败自动降级
|
||||||
|
- **📊 评论记录**:成功评论自动记录,今日/本月/总计统计,点击记录打开对应笔记
|
||||||
|
- **🔐 授权体系**:1 天试用(倒计时精确到秒)、卡密激活、插件内购买自动发卡
|
||||||
|
- **🧪 元素检测**:一键测试页面元素识别情况,方便小红书改版后快速排查
|
||||||
|
|
||||||
|
## 🚀 安装方法
|
||||||
|
|
||||||
|
1. 下载插件文件到本地(或 `git clone` 本仓库)
|
||||||
|
2. 打开 Chrome / Edge,进入扩展程序页面(`chrome://extensions/` 或 `edge://extensions/`)
|
||||||
|
3. 开启「开发者模式」
|
||||||
|
4. 点击「加载已解压的扩展程序」,选择插件文件夹
|
||||||
|
5. 工具栏出现红色「小红书」图标,点击即在右侧打开侧边栏
|
||||||
|
|
||||||
|
## 📖 使用方法
|
||||||
|
|
||||||
|
### 打开侧边栏
|
||||||
|
- 点击工具栏图标,界面**固定在浏览器右侧**,切换标签页不会关闭,方便边看边操作
|
||||||
|
- 再点一次图标可收起
|
||||||
|
|
||||||
|
### 授权与购买
|
||||||
|
1. 安装后自动进入 **1 天免费试用**(侧边栏顶部显示精确到秒的倒计时)
|
||||||
|
2. 试用结束后需激活:输入卡密点「激活」,或点「购买授权」
|
||||||
|
3. 购买流程:点购买 → 选支付宝/微信 → 新页面完成支付 → **支付成功自动发卡并激活生效**,无需手动输卡密
|
||||||
|
4. 已购用户也可在 [购买中心](https://auth.88531.cn/shop) 用订单号查询卡密,回来手动激活
|
||||||
|
5. 授权绑定本机浏览器,换设备请联系管理员重置
|
||||||
|
|
||||||
|
### 自动搜索评论
|
||||||
|
1. 输入搜索关键词
|
||||||
|
2. (可选)选择地区:如「北京」「自定义… 杭州西湖区」
|
||||||
|
3. 点击「🔍 搜索」→ 自动打开搜索结果页并开始评论流程,状态实时显示在面板内
|
||||||
|
|
||||||
|
> 说明:小红书网页版没有原生地区筛选,地区以关键词形式参与搜索(实际搜索词为「关键词 地区」)。
|
||||||
|
|
||||||
|
### 评论方式(四选一,改完立即生效)
|
||||||
|
- **固定评论池**:循环发预设短评,不调用模型
|
||||||
|
- **自定义文字**:文字库一行一条,循环发送
|
||||||
|
- **图片回复**:每篇笔记发一张图片,多张循环
|
||||||
|
- **AI 按笔记生成**:每篇笔记调用模型按内容生成
|
||||||
|
|
||||||
|
未配置对应内容时(如选了文字模式但库是空的),选项下方会给出橙色提示。
|
||||||
|
|
||||||
|
### 手动触发 / 元素检测
|
||||||
|
- 在小红书页面点「🤖 自动点击笔记」立即开始;点「🧪 测试元素检测」排查页面识别问题
|
||||||
|
|
||||||
|
### 配置 AI 评论(自定义模型)
|
||||||
|
1. 侧边栏「AI评论生成」处填写(改动即时保存):
|
||||||
|
- **API密钥**:服务商的 key(`sk-...`),没有?[👉 免费获取模型 KEY](https://ai.88531.cn)
|
||||||
|
- **API地址**:任意 OpenAI 兼容接口(未带 `/v1` 自动补全)
|
||||||
|
- **模型名**:如 `mimo-v2.6-flash`、`deepseek-ai/DeepSeek-R1`
|
||||||
|
2. 点「测试AI」验证连通,状态显示 🟢 已启用即可用
|
||||||
|
|
||||||
|
## 🔧 自动点击调试
|
||||||
|
|
||||||
|
1. **控制台日志**:F12 → Console 查看完整操作日志
|
||||||
|
2. **元素检测**:侧边栏点「🧪 测试元素检测」查看识别详情
|
||||||
|
3. **常见问题**:
|
||||||
|
- 页面未完全加载 → 等加载完成再启动
|
||||||
|
- 小红书改版 → 用元素检测确认选择器,更新 `content.js` 中的 CSS 选择器
|
||||||
|
- 评论没发出去 → 检查网络/登录状态,观察控制台报错
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
- 需要在小红书官网 (xiaohongshu.com) 登录状态下使用
|
||||||
|
- 评论内容请遵守平台规范,建议合理控制频率,避免账号风控
|
||||||
|
- 页面结构变化可能导致兼容性问题,欢迎提 Issue 反馈
|
||||||
|
|
||||||
|
## 📜 版本历史
|
||||||
|
|
||||||
|
当前版本:**1.7.5**
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>展开查看完整变更记录(v1.2.0 ~ v1.7.5)</summary>
|
||||||
|
|
||||||
|
### 1.7.5
|
||||||
|
- 试用倒计时精确到秒(HH:MM:SS 每秒刷新,归零自动锁定)
|
||||||
|
|
||||||
|
### 1.7.4
|
||||||
|
- 移除「发现新版本」提示条;底部新增「⬇️ 获取最新版本」大按钮直达官方下载页
|
||||||
|
|
||||||
|
### 1.7.3
|
||||||
|
- 修复更新提示残留;公告按「标题+内容+时间」展示
|
||||||
|
|
||||||
|
### 1.7.2
|
||||||
|
- 「AI评论生成」上方新增橙色发光横幅「免费获取模型KEY → ai.88531.cn」
|
||||||
|
|
||||||
|
### 1.7.1
|
||||||
|
- 图标改为小红书风格 PNG(品牌红底 + 白色「小红书」字样),工具栏一眼可认
|
||||||
|
|
||||||
|
### 1.7.0
|
||||||
|
- 接入 CardAuth 授权系统:1 天试用、卡密激活、插件内购买**自动发卡自动激活**
|
||||||
|
- 公告推送(弹窗每条一次 + 列表)、安装统计、30 分钟心跳
|
||||||
|
|
||||||
|
### 1.6.0
|
||||||
|
- 新增「自定义文字」「图片回复」两种评论方式(评论方式扩展为四选一)
|
||||||
|
|
||||||
|
### 1.5.0
|
||||||
|
- 新增「按地区搜索」(地区下拉 + 自定义,自动拼进关键词)
|
||||||
|
|
||||||
|
### 1.4.x
|
||||||
|
- 侧边栏常驻、评论方式开关、图标识别小字
|
||||||
|
|
||||||
|
### 1.3.x
|
||||||
|
- 侧边栏(Side Panel)改版;修复思考型模型 AI 正文为空的问题
|
||||||
|
|
||||||
|
### 1.2.0
|
||||||
|
- AI 模型自定义配置(API地址/模型名/密钥),请求改由后台发出规避跨域
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🛠 开发说明
|
||||||
|
|
||||||
|
纯 JavaScript 开发,**无任何外部依赖**,代码结构清晰,便于二次开发:
|
||||||
|
|
||||||
|
```
|
||||||
|
manifest.json # 扩展清单(MV3)
|
||||||
|
background.js # 后台服务:AI请求代理、授权/公告/心跳
|
||||||
|
content.js # 内容脚本:笔记点击、评论输入与提交、记录
|
||||||
|
sidepanel.html/js/css# 侧边栏 UI(搜索/评论配置/授权/记录)
|
||||||
|
icons/ # 16/32/48/128 小红书风格图标
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 免责声明
|
||||||
|
|
||||||
|
本插件仅供学习与效率研究使用。使用本插件产生的一切行为(评论内容、频率等)由使用者自行负责,请遵守小红书平台规范及相关法律法规。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**⬇️ [获取最新版本](https://dl.88531.cn/xhs.html)** · 反馈与合作请提 Issue
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
// 后台脚本
|
||||||
|
// 点击工具栏图标 → 直接在浏览器右侧打开/收起侧边栏(而不是弹窗)
|
||||||
|
if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
|
||||||
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((error) => {
|
||||||
|
console.log('设置侧边栏行为失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认自定义模型配置(OpenAI 兼容接口)
|
||||||
|
const DEFAULT_AI = {
|
||||||
|
apiBaseUrl: 'https://ai.88531.cn/v1',
|
||||||
|
model: 'mimo-v2.6-flash'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== CardAuth 授权系统配置 =====
|
||||||
|
const AUTH_BASE = 'https://auth.88531.cn';
|
||||||
|
const AUTH_PROJECT_ID = 3;
|
||||||
|
const AUTH_API_KEY = 'ak_bbf83b76df8219388d7f60b9e67c7db2';
|
||||||
|
const CLIENT_VERSION = '1.7.5';
|
||||||
|
const TRIAL_MS = 24 * 60 * 60 * 1000; // 未授权试用1天
|
||||||
|
|
||||||
|
// 把用户填写的地址规范化为完整的 chat/completions 端点
|
||||||
|
function buildChatCompletionsUrl(baseUrl) {
|
||||||
|
let base = String(baseUrl || '').trim() || DEFAULT_AI.apiBaseUrl;
|
||||||
|
base = base.replace(/\/+$/, '');
|
||||||
|
if (/\/chat\/completions$/i.test(base)) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
if (!/\/v\d+$/i.test(base)) {
|
||||||
|
base += '/v1'; // 未带版本号时自动补 /v1(如 https://api.deepseek.com)
|
||||||
|
}
|
||||||
|
return base + '/chat/completions';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造请求体
|
||||||
|
function buildChatBody(model, prompt, maxTokens) {
|
||||||
|
return {
|
||||||
|
model: model,
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
// 思考型模型(mimo / DeepSeek-R1 等)会先写 reasoning_content,
|
||||||
|
// 不关思考时正文容易被 max_tokens 截断 → 返回空。
|
||||||
|
// 支持的端点直接关掉思考;不支持的端点会报 400,下面自动去掉该参数重试
|
||||||
|
thinking: { type: 'disabled' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从各种 OpenAI 兼容返回结构里取正文(兼容 content 字符串 / 分段数组 / text / output_text 等)
|
||||||
|
function extractChatContent(data) {
|
||||||
|
if (!data || typeof data !== 'object') return '';
|
||||||
|
const pick = (value) => {
|
||||||
|
if (typeof value === 'string') return value.trim();
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(part => {
|
||||||
|
if (typeof part === 'string') return part;
|
||||||
|
if (part && typeof part === 'object') return part.text || part.content || '';
|
||||||
|
return '';
|
||||||
|
}).join('').trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const choice = Array.isArray(data.choices) ? data.choices[0] : null;
|
||||||
|
if (choice) {
|
||||||
|
const fromMessage = choice.message ? pick(choice.message.content) : '';
|
||||||
|
if (fromMessage) return fromMessage;
|
||||||
|
const fromText = pick(choice.text);
|
||||||
|
if (fromText) return fromText;
|
||||||
|
}
|
||||||
|
return pick(data.output_text) || pick(data.output);
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log('小红书自动评论助手已安装');
|
||||||
|
|
||||||
|
// 初始化存储
|
||||||
|
chrome.storage.local.get(['expandContent', 'commentText', 'apiKey', 'aiPrompt', 'apiBaseUrl', 'modelName'], (result) => {
|
||||||
|
if (result.expandContent === undefined) {
|
||||||
|
chrome.storage.local.set({ expandContent: true });
|
||||||
|
}
|
||||||
|
if (!result.commentText) {
|
||||||
|
chrome.storage.local.set({ commentText: '👍 赞!' });
|
||||||
|
}
|
||||||
|
if (!result.apiKey) {
|
||||||
|
chrome.storage.local.set({ apiKey: '' });
|
||||||
|
}
|
||||||
|
// 自定义模型配置:默认使用 ai.88531.cn/v1 的 mimo-v2.6-flash
|
||||||
|
if (!result.apiBaseUrl) {
|
||||||
|
chrome.storage.local.set({ apiBaseUrl: DEFAULT_AI.apiBaseUrl });
|
||||||
|
}
|
||||||
|
if (!result.modelName) {
|
||||||
|
chrome.storage.local.set({ modelName: DEFAULT_AI.model });
|
||||||
|
}
|
||||||
|
if (!result.aiPrompt) {
|
||||||
|
const defaultPrompt = '根据笔记内容来生成符合笔记的评论。最后只需要输出评论内容,不要输出你的推理,不要输出任何与评论无关的内容,不要输出#相关的内容,不要输出笔记关键词相关的内容。评论不要超过300字。这个是小红书的笔记内容:\n\n{{content}}\n\n';
|
||||||
|
chrome.storage.local.set({ aiPrompt: defaultPrompt });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 授权系统:登记安装(计入后台安装统计)+ 初始化试用期 + 拉公告
|
||||||
|
authOnInstalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 浏览器启动 / 服务工作线程重启时恢复定时任务
|
||||||
|
chrome.runtime.onStartup?.addListener?.(() => {
|
||||||
|
setupAuthAlarms();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== CardAuth 授权系统 =====
|
||||||
|
|
||||||
|
// 统一请求封装(CardAuth 统一响应: {code, message, data})
|
||||||
|
async function authFetch(path, { method = 'GET', body = null, needKey = false } = {}) {
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (needKey) headers['X-Api-Key'] = AUTH_API_KEY;
|
||||||
|
|
||||||
|
const response = await fetch(AUTH_BASE + path, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
let json = {};
|
||||||
|
try {
|
||||||
|
json = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
json = {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: response.ok && json.code === 200,
|
||||||
|
status: response.status,
|
||||||
|
code: json.code,
|
||||||
|
message: json.message || `HTTP ${response.status}`,
|
||||||
|
data: json.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取/生成设备指纹(授权绑定与安装统计的唯一标识)
|
||||||
|
async function getDeviceId() {
|
||||||
|
const result = await chrome.storage.local.get(['deviceId']);
|
||||||
|
if (result.deviceId) return result.deviceId;
|
||||||
|
const deviceId = 'EXT-' + crypto.randomUUID();
|
||||||
|
await chrome.storage.local.set({ deviceId });
|
||||||
|
console.log('已生成设备指纹:', deviceId);
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装初始化:试用期起点 + 设备指纹 + 安装上报 + 公告
|
||||||
|
async function authOnInstalled() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get(['trialStartedAt']);
|
||||||
|
if (!result.trialStartedAt) {
|
||||||
|
await chrome.storage.local.set({ trialStartedAt: Date.now() });
|
||||||
|
console.log('试用期开始(1天)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化试用期失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAuthAlarms();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authCheckUpdate(); // 后台计入安装统计 + 检查更新
|
||||||
|
} catch (error) {
|
||||||
|
console.error('安装上报失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authGetAnnouncements();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('拉取公告失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已有卡密则静默复验
|
||||||
|
try {
|
||||||
|
const { licenseState } = await chrome.storage.local.get(['licenseState']);
|
||||||
|
if (licenseState && licenseState.cardKey && licenseState.valid) {
|
||||||
|
await authVerify(licenseState.cardKey);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动时授权复验失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册定时任务:心跳/复验/公告/更新检查
|
||||||
|
function setupAuthAlarms() {
|
||||||
|
if (!chrome.alarms) return;
|
||||||
|
chrome.alarms.create('authHeartbeat', { periodInMinutes: 30 });
|
||||||
|
chrome.alarms.create('authVerify', { periodInMinutes: 10 });
|
||||||
|
chrome.alarms.create('authAnnounce', { periodInMinutes: 60 });
|
||||||
|
chrome.alarms.create('authUpdateCheck', { periodInMinutes: 60 });
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.alarms?.onAlarm?.addListener(async (alarm) => {
|
||||||
|
try {
|
||||||
|
if (alarm.name === 'authHeartbeat') {
|
||||||
|
await authHeartbeat();
|
||||||
|
} else if (alarm.name === 'authVerify') {
|
||||||
|
const { licenseState } = await chrome.storage.local.get(['licenseState']);
|
||||||
|
if (licenseState && licenseState.cardKey && licenseState.valid) {
|
||||||
|
await authVerify(licenseState.cardKey);
|
||||||
|
}
|
||||||
|
} else if (alarm.name === 'authAnnounce') {
|
||||||
|
await authGetAnnouncements();
|
||||||
|
} else if (alarm.name === 'authUpdateCheck') {
|
||||||
|
await authCheckUpdate();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('授权定时任务失败:', alarm.name, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 安装上报 / 检查更新(后台「安装统计」自动记录)
|
||||||
|
async function authCheckUpdate() {
|
||||||
|
const machineId = await getDeviceId();
|
||||||
|
const { licenseState } = await chrome.storage.local.get(['licenseState']);
|
||||||
|
const body = {
|
||||||
|
machine_id: machineId,
|
||||||
|
client_version: CLIENT_VERSION,
|
||||||
|
device_info: navigator.userAgent.includes('Edg/') ? 'Edge 插件' : 'Chrome 插件'
|
||||||
|
};
|
||||||
|
if (licenseState && licenseState.cardKey) body.card_key = licenseState.cardKey;
|
||||||
|
|
||||||
|
const res = await authFetch('/api/public/installations/check-update', {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
needKey: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok && res.data) {
|
||||||
|
const updateInfo = {
|
||||||
|
updateAvailable: !!res.data.update_available,
|
||||||
|
updateRequired: !!res.data.update_required,
|
||||||
|
latestVersion: res.data.latest_version || '',
|
||||||
|
downloadUrl: res.data.download_url || '',
|
||||||
|
changelog: res.data.changelog || '',
|
||||||
|
isForce: !!res.data.is_force,
|
||||||
|
checkedAt: Date.now()
|
||||||
|
};
|
||||||
|
await chrome.storage.local.set({ updateInfo });
|
||||||
|
console.log('更新检查:', updateInfo.updateAvailable ? `发现 ${updateInfo.latestVersion}` : '已是最新');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心跳:更新「最后上线」时间
|
||||||
|
async function authHeartbeat() {
|
||||||
|
const machineId = await getDeviceId();
|
||||||
|
const { licenseState } = await chrome.storage.local.get(['licenseState']);
|
||||||
|
const body = {
|
||||||
|
machine_id: machineId,
|
||||||
|
client_version: CLIENT_VERSION
|
||||||
|
};
|
||||||
|
if (licenseState && licenseState.cardKey) body.card_key = licenseState.cardKey;
|
||||||
|
|
||||||
|
return authFetch('/api/public/installations/heartbeat', {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
needKey: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权验证:cardKey 存在则校验并更新缓存;首次调用即绑定设备
|
||||||
|
async function authVerify(cardKey) {
|
||||||
|
const key = String(cardKey || '').trim();
|
||||||
|
if (!key) return { ok: false, message: '卡密不能为空' };
|
||||||
|
|
||||||
|
const machineId = await getDeviceId();
|
||||||
|
const res = await authFetch('/api/public/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
card_key: key,
|
||||||
|
machine_id: machineId,
|
||||||
|
client_version: CLIENT_VERSION,
|
||||||
|
device_info: '小红书评论插件 ' + CLIENT_VERSION
|
||||||
|
},
|
||||||
|
needKey: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// 网络/服务异常时不清除现有授权,仅报错
|
||||||
|
return { ok: false, message: res.message || '验证失败' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = res.data || {};
|
||||||
|
const licenseState = {
|
||||||
|
valid: !!data.valid,
|
||||||
|
cardKey: key,
|
||||||
|
message: data.message || '',
|
||||||
|
expireTime: data.expire_time || '',
|
||||||
|
isPermanent: !!data.is_permanent,
|
||||||
|
remainingDays: data.remaining_days ?? null,
|
||||||
|
boundAt: data.bound_at || '',
|
||||||
|
verifiedAt: Date.now()
|
||||||
|
};
|
||||||
|
await chrome.storage.local.set({ licenseState });
|
||||||
|
console.log('授权验证:', licenseState.valid ? '有效' : '无效', licenseState.message);
|
||||||
|
return { ok: true, licenseState };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉取公告并缓存
|
||||||
|
async function authGetAnnouncements() {
|
||||||
|
const res = await authFetch('/api/public/announcements');
|
||||||
|
if (res.ok && res.data) {
|
||||||
|
const announcements = {
|
||||||
|
list: res.data.list || [],
|
||||||
|
popup: res.data.popup || [],
|
||||||
|
fetchedAt: Date.now()
|
||||||
|
};
|
||||||
|
await chrome.storage.local.set({ announcements });
|
||||||
|
return { ok: true, announcements };
|
||||||
|
}
|
||||||
|
return { ok: false, message: res.message || '拉取公告失败' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取授权状态(valid授权 > 1天试用 > 锁定)
|
||||||
|
async function authGetStatus() {
|
||||||
|
const { licenseState, trialStartedAt } = await chrome.storage.local.get(['licenseState', 'trialStartedAt']);
|
||||||
|
|
||||||
|
if (licenseState && licenseState.valid) {
|
||||||
|
return {
|
||||||
|
state: 'authorized',
|
||||||
|
license: {
|
||||||
|
expireTime: licenseState.expireTime,
|
||||||
|
isPermanent: licenseState.isPermanent,
|
||||||
|
remainingDays: licenseState.remainingDays,
|
||||||
|
cardKey: licenseState.cardKey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = trialStartedAt || Date.now();
|
||||||
|
if (!trialStartedAt) {
|
||||||
|
await chrome.storage.local.set({ trialStartedAt: start });
|
||||||
|
}
|
||||||
|
const remainingMs = start + TRIAL_MS - Date.now();
|
||||||
|
if (remainingMs > 0) {
|
||||||
|
return {
|
||||||
|
state: 'trial',
|
||||||
|
remainingMs,
|
||||||
|
remainingHours: Math.max(1, Math.ceil(remainingMs / 3600000)),
|
||||||
|
trialEndAt: start + TRIAL_MS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { state: 'locked' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建购买订单(contact_info 带机器码 → 支付成功后系统自动发卡绑定)
|
||||||
|
async function authCreateOrder(payType) {
|
||||||
|
const pay = payType === 'wxpay' ? 'wxpay' : 'alipay';
|
||||||
|
|
||||||
|
// 取项目第一个套餐
|
||||||
|
const typesRes = await authFetch(`/api/public/projects/${AUTH_PROJECT_ID}/card-types`);
|
||||||
|
if (!typesRes.ok || !Array.isArray(typesRes.data) || typesRes.data.length === 0) {
|
||||||
|
return { ok: false, message: '暂无在售套餐,请稍后再试' };
|
||||||
|
}
|
||||||
|
const cardType = typesRes.data[0];
|
||||||
|
|
||||||
|
const machineId = await getDeviceId();
|
||||||
|
const res = await authFetch('/api/public/orders', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
project_id: AUTH_PROJECT_ID,
|
||||||
|
card_type_id: cardType.id,
|
||||||
|
amount: Number(cardType.price),
|
||||||
|
pay_type: pay,
|
||||||
|
contact_info: `MACHINE:${machineId}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok || !res.data) {
|
||||||
|
return { ok: false, message: res.message || '创建订单失败' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录进行中的订单,便于侧边栏恢复轮询
|
||||||
|
const pendingOrder = {
|
||||||
|
orderNo: res.data.order_no,
|
||||||
|
payType: pay,
|
||||||
|
amount: Number(cardType.price),
|
||||||
|
packageName: cardType.name || '',
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
await chrome.storage.local.set({ pendingOrder });
|
||||||
|
|
||||||
|
return { ok: true, order: pendingOrder, payUrl: res.data.pay_url };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询订单(支付+自动发卡后返回 card_key;拿到卡密自动激活)
|
||||||
|
async function authQueryOrder(orderNo) {
|
||||||
|
const res = await authFetch(`/api/public/orders/query?order_no=${encodeURIComponent(orderNo)}`);
|
||||||
|
if (!res.ok || !res.data) {
|
||||||
|
return { ok: false, message: res.message || '订单查询失败' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = res.data;
|
||||||
|
const result = {
|
||||||
|
ok: true,
|
||||||
|
paid: data.status === 'paid' || !!data.card_key,
|
||||||
|
status: data.status,
|
||||||
|
cardKey: data.card_key || '',
|
||||||
|
expireTime: data.expire_time || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动发卡成功 → 自动激活绑定本机
|
||||||
|
if (result.cardKey) {
|
||||||
|
const verifyRes = await authVerify(result.cardKey);
|
||||||
|
result.activated = !!(verifyRes.ok && verifyRes.licenseState && verifyRes.licenseState.valid);
|
||||||
|
result.licenseState = verifyRes.licenseState;
|
||||||
|
if (result.activated) {
|
||||||
|
await chrome.storage.local.remove(['pendingOrder']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自内容脚本的消息
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.action === 'getCurrentNote') {
|
||||||
|
handleGetCurrentNote(request, sendResponse);
|
||||||
|
return true; // 保持消息通道开放
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'saveNote') {
|
||||||
|
handleSaveNote(request.noteData, sendResponse);
|
||||||
|
return true; // 保持消息通道开放
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'callAiApi') {
|
||||||
|
handleCallAiApi(request, sendResponse);
|
||||||
|
return true; // 保持消息通道开放
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'getNotes') {
|
||||||
|
handleGetNotes(sendResponse);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'deleteNote') {
|
||||||
|
handleDeleteNote(request.noteId, sendResponse);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 授权系统消息 =====
|
||||||
|
if (request.action === 'authStatus') {
|
||||||
|
authGetStatus().then((status) => sendResponse({ ok: true, status }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'authActivate') {
|
||||||
|
authVerify(request.cardKey).then((res) => sendResponse(res));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'authCreateOrder') {
|
||||||
|
authCreateOrder(request.payType).then((res) => sendResponse(res));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'authQueryOrder') {
|
||||||
|
authQueryOrder(request.orderNo).then((res) => sendResponse(res));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'authGetAnnouncements') {
|
||||||
|
authGetAnnouncements().then((res) => sendResponse(res)).catch((error) => {
|
||||||
|
sendResponse({ ok: false, message: error.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'authCheckUpdate') {
|
||||||
|
authCheckUpdate()
|
||||||
|
.then(async () => {
|
||||||
|
const { updateInfo } = await chrome.storage.local.get(['updateInfo']);
|
||||||
|
sendResponse({ ok: true, updateInfo: updateInfo || null });
|
||||||
|
})
|
||||||
|
.catch((error) => sendResponse({ ok: false, message: error.message }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 调用自定义模型(OpenAI 兼容接口),由后台执行以规避页面 CORS / 混合内容限制
|
||||||
|
async function handleCallAiApi(request, sendResponse) {
|
||||||
|
const apiKey = String(request.apiKey || '').trim();
|
||||||
|
if (!apiKey) {
|
||||||
|
sendResponse({ ok: false, error: '未配置API密钥' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildChatCompletionsUrl(request.baseUrl);
|
||||||
|
const model = String(request.model || '').trim() || DEFAULT_AI.model;
|
||||||
|
const prompt = String(request.prompt || '');
|
||||||
|
const maxTokens = Number(request.maxTokens) > 0 ? Number(request.maxTokens) : 300;
|
||||||
|
const timeoutMs = Number(request.timeoutMs) > 0 ? Number(request.timeoutMs) : 90000;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = buildChatBody(model, prompt, maxTokens);
|
||||||
|
|
||||||
|
const sendOnce = async (payload) => {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
let data = {};
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (parseError) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
return { response: response, data: data };
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await sendOnce(body);
|
||||||
|
|
||||||
|
// 400/422:端点不认某些参数,按报错修正后重试一次
|
||||||
|
if (!result.response.ok && (result.response.status === 400 || result.response.status === 422)) {
|
||||||
|
const message = (result.data && result.data.error && result.data.error.message) || '';
|
||||||
|
let repaired = false;
|
||||||
|
|
||||||
|
if (body.thinking && /thinking|unknown|unrecognized|extra|invalid|not allowed|parameter/i.test(message)) {
|
||||||
|
delete body.thinking;
|
||||||
|
repaired = true;
|
||||||
|
}
|
||||||
|
if (body.max_tokens && /max_tokens/i.test(message) && /max_completion_tokens|unknown|unrecognized|invalid|not allowed/i.test(message)) {
|
||||||
|
body.max_completion_tokens = body.max_tokens;
|
||||||
|
delete body.max_tokens;
|
||||||
|
repaired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repaired) {
|
||||||
|
console.log('参数不被端点接受,修正后重试:', message);
|
||||||
|
result = await sendOnce(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.response.ok) {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
status: result.response.status,
|
||||||
|
error: (result.data && result.data.error && result.data.error.message) || `HTTP ${result.response.status}`,
|
||||||
|
url: url,
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = extractChatContent(result.data);
|
||||||
|
|
||||||
|
// 正文为空:基本是 max_tokens 被思考过程吃掉 → 提高上限重试一次
|
||||||
|
if (!content) {
|
||||||
|
const choice = result.data && Array.isArray(result.data.choices) ? result.data.choices[0] : null;
|
||||||
|
const truncated = !!(choice && choice.finish_reason === 'length');
|
||||||
|
const hasReasoning = !!(choice && choice.message && typeof choice.message.reasoning_content === 'string' && choice.message.reasoning_content.length > 0);
|
||||||
|
|
||||||
|
if (truncated || hasReasoning) {
|
||||||
|
body.max_tokens = Math.max(4096, maxTokens * 4);
|
||||||
|
console.log('正文为空,提高 max_tokens 到', body.max_tokens, '重试');
|
||||||
|
result = await sendOnce(body);
|
||||||
|
if (result.response.ok) {
|
||||||
|
content = extractChatContent(result.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: hasReasoning
|
||||||
|
? '模型只返回了思考过程,没有返回正文(可提高 max_tokens 或换非思考型模型)'
|
||||||
|
: 'AI返回的内容为空',
|
||||||
|
url: url,
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse({ ok: true, content: content, url: url, model: model });
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.name === 'AbortError' ? `请求超时(${Math.round(timeoutMs / 1000)}秒)` : error.message,
|
||||||
|
url: url,
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetCurrentNote(request, sendResponse) {
|
||||||
|
try {
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
message: '获取当前笔记成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取当前笔记失败:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveNote(request, sendResponse) {
|
||||||
|
try {
|
||||||
|
sendResponse({ success: true, message: '保存笔记成功' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存笔记失败:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetNotes(sendResponse) {
|
||||||
|
try {
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
notes: [],
|
||||||
|
tags: []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取笔记失败:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteNote(request, sendResponse) {
|
||||||
|
try {
|
||||||
|
sendResponse({ success: true, message: '删除笔记成功' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除笔记失败:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储变化监听
|
||||||
|
chrome.storage.onChanged.addListener((changes, namespace) => {
|
||||||
|
if (namespace === 'local') {
|
||||||
|
console.log('存储数据发生变化:', changes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标签页更新监听
|
||||||
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
|
if (changeInfo.status === 'complete' && tab.url && tab.url.includes('xiaohongshu.com')) {
|
||||||
|
// 当小红书页面加载完成时,可以在这里执行一些操作
|
||||||
|
console.log('小红书页面加载完成');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
/* 小红书笔记收藏按钮样式 */
|
||||||
|
.xhs-save-btn {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 50% !important;
|
||||||
|
right: 20px !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
background: linear-gradient(135deg, #ff6b6b, #ff8e8e) !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 50px !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
font-family: 'Segoe UI', sans-serif !important;
|
||||||
|
transform: translateY(-50%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-save-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #ff5252, #ff7b7b) !important;
|
||||||
|
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4) !important;
|
||||||
|
transform: translateY(-50%) scale(1.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-save-btn.saved {
|
||||||
|
background: linear-gradient(135deg, #4caf50, #66bb6a) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-save-btn.saved:hover {
|
||||||
|
background: linear-gradient(135deg, #45a049, #5db85d) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-toast {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 20px !important;
|
||||||
|
right: 20px !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
color: white !important;
|
||||||
|
padding: 12px 20px !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
animation: slideIn 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-modal {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
background: rgba(0, 0, 0, 0.5) !important;
|
||||||
|
z-index: 10001 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-modal-content {
|
||||||
|
background: white !important;
|
||||||
|
padding: 24px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
width: 400px !important;
|
||||||
|
max-width: 90% !important;
|
||||||
|
max-height: 80% !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-modal h3 {
|
||||||
|
margin: 0 0 16px 0 !important;
|
||||||
|
color: #333 !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
border: 1px solid #ddd !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-list {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-item {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
border: 1px solid #ddd !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-item:hover {
|
||||||
|
background: #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-tag-item.selected {
|
||||||
|
background: #ff6b6b !important;
|
||||||
|
color: white !important;
|
||||||
|
border-color: #ff6b6b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-modal-actions {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
justify-content: flex-end !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-modal-btn {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-primary-btn {
|
||||||
|
background: #ff6b6b !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-primary-btn:hover {
|
||||||
|
background: #ff5252 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-secondary-btn {
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-secondary-btn:hover {
|
||||||
|
background: #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条样式 */
|
||||||
|
.xhs-progress-bar {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 20px !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translateX(-50%) !important;
|
||||||
|
z-index: 10002 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.9) !important;
|
||||||
|
color: white !important;
|
||||||
|
padding: 16px 24px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
min-width: 300px !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
transition: opacity 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-bar.show {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-container {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-text {
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #4caf50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-bar-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 6px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-fill {
|
||||||
|
height: 100% !important;
|
||||||
|
background: linear-gradient(90deg, #4caf50, #66bb6a) !important;
|
||||||
|
width: 0% !important;
|
||||||
|
transition: width 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-stats {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
color: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-tip {
|
||||||
|
text-align: center !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
color: #888 !important;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
font-style: italic !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-stop-btn {
|
||||||
|
background: #f44336 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
transition: background 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-stop-btn:hover {
|
||||||
|
background: #d32f2f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-progress-actions {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
margin-top: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-test-stop-btn {
|
||||||
|
background: #ff9800 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
transition: background 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-test-stop-btn:hover {
|
||||||
|
background: #f57c00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义评论管理弹窗样式 */
|
||||||
|
.xhs-comment-modal {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
background: rgba(0, 0, 0, 0.5) !important;
|
||||||
|
z-index: 10003 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-modal-content {
|
||||||
|
background: white !important;
|
||||||
|
padding: 24px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
width: 500px !important;
|
||||||
|
max-width: 90% !important;
|
||||||
|
max-height: 80% !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-modal h3 {
|
||||||
|
margin: 0 0 16px 0 !important;
|
||||||
|
color: #333 !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-input-area {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-input {
|
||||||
|
flex: 1 !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
border: 1px solid #ddd !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-add-comment-btn {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
background: #4caf50 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
transition: background 0.3s ease !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-add-comment-btn:hover {
|
||||||
|
background: #45a049 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-list {
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-list-header {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #333 !important;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-items {
|
||||||
|
max-height: 200px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
border: 1px solid #eee !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-item {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
border-bottom: 1px solid #f0f0f0 !important;
|
||||||
|
transition: background 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-item:last-child {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-item:hover {
|
||||||
|
background: #f9f9f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-text {
|
||||||
|
flex: 1 !important;
|
||||||
|
margin-right: 8px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
color: #333 !important;
|
||||||
|
word-break: break-all !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-delete-comment-btn {
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
background: #f44336 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
transition: background 0.3s ease !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-delete-comment-btn:hover {
|
||||||
|
background: #d32f2f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-no-comments {
|
||||||
|
text-align: center !important;
|
||||||
|
color: #999 !important;
|
||||||
|
font-style: italic !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 评论模式切换按钮样式 */
|
||||||
|
.xhs-comment-mode-btn {
|
||||||
|
background: #2196f3 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-mode-btn:hover {
|
||||||
|
background: #1976d2 !important;
|
||||||
|
transform: scale(1.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-mode-btn.xhs-secondary-btn {
|
||||||
|
background: #ff9800 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-comment-mode-btn.xhs-secondary-btn:hover {
|
||||||
|
background: #f57c00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 清理内存按钮样式 */
|
||||||
|
.xhs-cleanup-btn {
|
||||||
|
background: #9c27b0 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
transition: background 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xhs-cleanup-btn:hover {
|
||||||
|
background: #7b1fa2 !important;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" rx="16" fill="url(#gradient)"/>
|
||||||
|
<rect x="32" y="14" width="64" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="32" y="26" width="48" height="6" rx="3" fill="white"/>
|
||||||
|
<rect x="32" y="38" width="32" height="6" rx="3" fill="white"/>
|
||||||
|
<text x="64" y="76" text-anchor="middle" font-family="'PingFang SC','Microsoft YaHei','Noto Sans CJK SC','Heiti SC',sans-serif" font-size="28" font-weight="700" fill="white">小红书</text>
|
||||||
|
<circle cx="96" cy="110" r="13" fill="white"/>
|
||||||
|
<path d="M91 110l3.5 3.5 7-7" stroke="#ff6b6b" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ff6b6b"/>
|
||||||
|
<stop offset="100%" style="stop-color:#ff8e8e"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 961 B |
|
After Width: | Height: | Size: 376 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="16" height="16" rx="2" fill="#ff6b6b"/>
|
||||||
|
<text x="8" y="11.6" text-anchor="middle" font-family="'PingFang SC','Microsoft YaHei','Noto Sans CJK SC','Heiti SC',sans-serif" font-size="10" font-weight="700" fill="white">红</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 346 B |
|
After Width: | Height: | Size: 619 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="32" height="32" rx="4" fill="#ff6b6b"/>
|
||||||
|
<rect x="6" y="3.5" width="14" height="1.6" rx="0.8" fill="white"/>
|
||||||
|
<rect x="6" y="7" width="10" height="1.6" rx="0.8" fill="white"/>
|
||||||
|
<rect x="6" y="10.5" width="6" height="1.6" rx="0.8" fill="white"/>
|
||||||
|
<text x="14" y="20.5" text-anchor="middle" font-family="'PingFang SC','Microsoft YaHei','Noto Sans CJK SC','Heiti SC',sans-serif" font-size="8" font-weight="700" fill="white">小红书</text>
|
||||||
|
<circle cx="26.5" cy="26.5" r="3.5" fill="white"/>
|
||||||
|
<path d="M25 26.5l1 1 2-2" stroke="#ff6b6b" stroke-width="1.1" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 739 B |
|
After Width: | Height: | Size: 819 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="48" height="48" rx="6" fill="#ff6b6b"/>
|
||||||
|
<rect x="12" y="6" width="24" height="3" rx="1.5" fill="white"/>
|
||||||
|
<rect x="12" y="11" width="18" height="3" rx="1.5" fill="white"/>
|
||||||
|
<rect x="12" y="16" width="12" height="3" rx="1.5" fill="white"/>
|
||||||
|
<text x="24" y="30" text-anchor="middle" font-family="'PingFang SC','Microsoft YaHei','Noto Sans CJK SC','Heiti SC',sans-serif" font-size="11" font-weight="700" fill="white">小红书</text>
|
||||||
|
<circle cx="37" cy="40" r="5" fill="white"/>
|
||||||
|
<path d="M35 40l1.5 1.5 3-3" stroke="#ff6b6b" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 729 B |
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "小红书自动评论助手",
|
||||||
|
"version": "1.7.5",
|
||||||
|
"description": "右侧侧边栏常驻的自动评论助手,支持试用/卡密授权、固定池/自定义文字/图片/AI评论",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"unlimitedStorage",
|
||||||
|
"activeTab",
|
||||||
|
"scripting",
|
||||||
|
"tabs",
|
||||||
|
"sidePanel",
|
||||||
|
"alarms"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://www.xiaohongshu.com/*",
|
||||||
|
"https://*/*",
|
||||||
|
"http://*/*"
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_title": "打开小红书自动评论助手侧边栏"
|
||||||
|
},
|
||||||
|
"side_panel": {
|
||||||
|
"default_path": "sidepanel.html"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://www.xiaohongshu.com/*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"css": ["content.css"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>小红书自动评论助手</title>
|
||||||
|
<link rel="stylesheet" href="sidepanel.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🤖 小红书自动评论助手</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-section">
|
||||||
|
<div class="auth-status-row">
|
||||||
|
<span class="auth-badge" id="auth-badge">…</span>
|
||||||
|
<span class="auth-detail" id="auth-detail">正在检查授权状态…</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-actions">
|
||||||
|
<input type="text" id="card-key-input" class="card-key-input" placeholder="输入卡密,如 CA-XXXX-XXXX" />
|
||||||
|
<button id="activate-btn" class="activate-btn">激活</button>
|
||||||
|
<button id="buy-btn" class="buy-btn">购买授权</button>
|
||||||
|
</div>
|
||||||
|
<div class="pay-select-row" id="pay-select-row" hidden>
|
||||||
|
<span class="pay-select-label">选择支付方式:</span>
|
||||||
|
<button class="pay-btn" data-pay="alipay">支付宝</button>
|
||||||
|
<button class="pay-btn" data-pay="wxpay">微信</button>
|
||||||
|
<button class="pay-btn pay-cancel" id="pay-cancel-btn">取消</button>
|
||||||
|
</div>
|
||||||
|
<div class="order-status" id="order-status"></div>
|
||||||
|
<div class="announcement-box" id="announcement-box" hidden>
|
||||||
|
<div class="announcement-title">📢 公告</div>
|
||||||
|
<div id="announcement-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-form">
|
||||||
|
<input type="text" id="keyword-input" placeholder="输入关键词搜索小红书..." />
|
||||||
|
<button id="search-btn">🔍 搜索</button>
|
||||||
|
</div>
|
||||||
|
<div class="region-row">
|
||||||
|
<label for="region-select">📍 按地区</label>
|
||||||
|
<select id="region-select">
|
||||||
|
<option value="">不限地区</option>
|
||||||
|
<option value="北京">北京</option>
|
||||||
|
<option value="上海">上海</option>
|
||||||
|
<option value="广州">广州</option>
|
||||||
|
<option value="深圳">深圳</option>
|
||||||
|
<option value="杭州">杭州</option>
|
||||||
|
<option value="成都">成都</option>
|
||||||
|
<option value="重庆">重庆</option>
|
||||||
|
<option value="武汉">武汉</option>
|
||||||
|
<option value="西安">西安</option>
|
||||||
|
<option value="南京">南京</option>
|
||||||
|
<option value="天津">天津</option>
|
||||||
|
<option value="苏州">苏州</option>
|
||||||
|
<option value="长沙">长沙</option>
|
||||||
|
<option value="郑州">郑州</option>
|
||||||
|
<option value="青岛">青岛</option>
|
||||||
|
<option value="厦门">厦门</option>
|
||||||
|
<option value="昆明">昆明</option>
|
||||||
|
<option value="沈阳">沈阳</option>
|
||||||
|
<option value="__custom__">自定义…</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="region-custom" class="region-custom-input" placeholder="输入自定义地区,如 杭州西湖区" hidden />
|
||||||
|
<small class="region-hint">选择地区后会自动拼进搜索关键词(如「咖啡 上海」),选择会自动保存</small>
|
||||||
|
<div class="action-status-row">
|
||||||
|
<span id="action-status" class="action-status"></span>
|
||||||
|
</div>
|
||||||
|
<div class="auto-click-controls">
|
||||||
|
<button id="auto-click-btn" class="auto-click-btn" title="在当前页面自动点击笔记">
|
||||||
|
🤖 自动点击笔记
|
||||||
|
</button>
|
||||||
|
<button id="test-detection-btn" class="test-btn" title="测试页面元素检测">
|
||||||
|
🧪 测试元素检测
|
||||||
|
</button>
|
||||||
|
<div class="click-mode-selector">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="expand-content" checked>
|
||||||
|
展开笔记内容
|
||||||
|
</label>
|
||||||
|
<small>勾选后会在点击笔记时展开内容,取消勾选则只简单点击</small>
|
||||||
|
</div>
|
||||||
|
<div class="mode-selector">
|
||||||
|
<div class="mode-label">评论方式</div>
|
||||||
|
<div class="mode-options">
|
||||||
|
<label class="mode-option" for="mode-custom">
|
||||||
|
<input type="radio" name="comment-mode" id="mode-custom" value="custom">
|
||||||
|
<span>固定评论池</span>
|
||||||
|
</label>
|
||||||
|
<label class="mode-option" for="mode-text">
|
||||||
|
<input type="radio" name="comment-mode" id="mode-text" value="text">
|
||||||
|
<span>自定义文字</span>
|
||||||
|
</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">
|
||||||
|
<input type="radio" name="comment-mode" id="mode-ai" value="ai">
|
||||||
|
<span>AI 按笔记生成</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small id="mode-hint">固定评论池 = 循环发预设短评;AI = 按每篇笔记内容生成</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-comment-config" id="text-comment-panel">
|
||||||
|
<label for="user-comments">自定义文字回复(一行一条,按顺序循环发送)</label>
|
||||||
|
<textarea id="user-comments" class="prompt-textarea" rows="5" placeholder="每行输入一条评论,例如: 这个宝藏笔记必须赞 收藏了,周末就去打卡"></textarea>
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button id="save-user-comments-btn" class="save-prompt-btn">保存文字库</button>
|
||||||
|
<span id="user-comments-count" class="panel-count"></span>
|
||||||
|
</div>
|
||||||
|
<small>改动即时保存已打开的小红书页面;一行一条,发完最后一条从头开始循环</small>
|
||||||
|
</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">
|
||||||
|
🔑 还没有模型KEY?<span class="ai-key-guide-cta">免费获取模型KEY</span> → ai.88531.cn
|
||||||
|
</a>
|
||||||
|
<div class="comment-config">
|
||||||
|
<label>
|
||||||
|
AI评论生成(自定义模型):
|
||||||
|
<div class="ai-config">
|
||||||
|
<input type="text" id="api-key" placeholder="API密钥" class="api-key-input">
|
||||||
|
</div>
|
||||||
|
<div class="ai-config">
|
||||||
|
<input type="text" id="api-base-url" placeholder="API地址,如 https://ai.88531.cn/v1" class="api-key-input">
|
||||||
|
</div>
|
||||||
|
<div class="ai-config">
|
||||||
|
<input type="text" id="model-name" placeholder="模型名,如 mimo-v2.6-flash" class="api-key-input">
|
||||||
|
<button id="test-ai-btn" class="test-ai-btn">测试AI</button>
|
||||||
|
</div>
|
||||||
|
<div class="test-result-row">
|
||||||
|
<span id="test-ai-result" class="test-ai-result"></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<small>任意 OpenAI 兼容接口均可(地址 + 模型名 + 密钥),改动即时保存</small>
|
||||||
|
<div class="ai-status" id="ai-status">
|
||||||
|
<span class="status-indicator">🔴</span>
|
||||||
|
<span class="status-text">未配置API密钥</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-config">
|
||||||
|
<label for="ai-prompt">AI 提示词(使用 {{content}} 作为笔记内容占位符)</label>
|
||||||
|
<textarea id="ai-prompt" class="prompt-textarea" rows="6" placeholder="输入用于生成评论的提示词..."></textarea>
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button id="save-prompt-btn" class="save-prompt-btn">保存提示词</button>
|
||||||
|
<button id="reset-prompt-btn" class="reset-prompt-btn">恢复默认</button>
|
||||||
|
</div>
|
||||||
|
<small>示例:根据笔记内容生成引流评论...\\n笔记内容:{{content}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-section">
|
||||||
|
<div class="log-header">
|
||||||
|
<h3>📊 评论记录</h3>
|
||||||
|
<button id="refresh-log-btn" class="log-refresh-btn" title="刷新记录">刷新</button>
|
||||||
|
</div>
|
||||||
|
<div class="log-stats">
|
||||||
|
<span class="log-stat"><strong id="stat-today">0</strong> 今日</span>
|
||||||
|
<span class="log-stat"><strong id="stat-month">0</strong> 本月</span>
|
||||||
|
<span class="log-stat"><strong id="stat-total">0</strong> 总计</span>
|
||||||
|
</div>
|
||||||
|
<div id="log-list" class="log-list">
|
||||||
|
<div class="log-empty">暂无评论记录,成功评论后会自动出现在这里</div>
|
||||||
|
</div>
|
||||||
|
<small class="log-note">仅显示最近 100 条(最多保留 300 条),点击记录可直接打开对应笔记</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-content">
|
||||||
|
<h3>📋 功能说明</h3>
|
||||||
|
<ul>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<h3>⚡ 使用方法</h3>
|
||||||
|
<ol>
|
||||||
|
<li>点击工具栏图标打开右侧侧边栏</li>
|
||||||
|
<li>在小红书页面输入搜索关键词</li>
|
||||||
|
<li>点击"🔍 搜索"开始自动操作</li>
|
||||||
|
<li>或直接点击"🤖 自动点击笔记"</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="version-info">
|
||||||
|
<span>版本: 1.7.5</span>
|
||||||
|
<span>侧边栏常驻 · 专注于自动评论</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="latest-version-bar">
|
||||||
|
<a class="latest-version-btn" href="https://dl.88531.cn/xhs.html" target="_blank" rel="noopener" title="点击前往下载最新版本">
|
||||||
|
⬇️ 获取最新版本
|
||||||
|
</a>
|
||||||
|
<div class="latest-version-tip">版本更新 / 下载安装包请认准官方地址 dl.88531.cn</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="sidepanel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||