788 lines
28 KiB
JavaScript
788 lines
28 KiB
JavaScript
// 后台脚本
|
||
// 点击工具栏图标 → 直接在浏览器右侧打开/收起侧边栏(而不是弹窗)
|
||
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('小红书页面加载完成');
|
||
}
|
||
}); |