// 后台脚本 // 点击工具栏图标 → 直接在浏览器右侧打开/收起侧边栏(而不是弹窗) 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.9'; 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 端点 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('试用期开始(3 天 · 上限 ' + TRIAL_QUOTA + ' 条)'); } } 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授权 > 72 小时试用 > 到期降级体验,不硬锁) 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 usage = await getUsage(); const trialEndAt = start + TRIAL_MS; const timeUp = Date.now() >= trialEndAt; const quotaReached = usage.quotaHitAt > 0; // 试用期内:时间没到 且 评论额度没用完 if (!timeUp && !quotaReached) { return { state: 'trial', remainingMs: trialEndAt - Date.now(), remainingHours: Math.max(1, Math.ceil((trialEndAt - Date.now()) / 3600000)), trialEndAt, quotaTotal: TRIAL_QUOTA, quotaUsed: usage.total, quotaLeft: Math.max(0, TRIAL_QUOTA - usage.total) }; } // 到期(时间到 或 额度用完)→ 降级体验:不硬锁,保留数据 + 每天 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 带机器码 → 支付成功后系统自动发卡绑定) 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 price = Number(cardType.price); // 试用到期 24 小时内下单自动带限时优惠码(服务端按折扣重算实付金额) const status = await authGetStatus(); const useCoupon = !!(status.state === 'grace' && status.promo && status.promo.active); const orderBody = () => ({ project_id: AUTH_PROJECT_ID, card_type_id: cardType.id, amount: price, pay_type: pay, 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) { 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 = { orderNo: res.data.order_no, payType: pay, amount: serverPay != null ? serverPay : fallbackPay, originalPrice: price, couponCode: couponApplied ? PROMO.code : '', 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 === 'usageTick') { usageTick().then((data) => sendResponse({ ok: true, data })); 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('小红书页面加载完成'); } });