v1.6.6: 使用说明+官网页链接、下载改夸克网盘、评论3字下限、AI读取视频真实内容、模拟真人逐字评论
This commit is contained in:
commit
38b90c1448
12 files changed
+2614
No files matched your search
+585
@@ -0,0 +1,585 @@
|
||||
// background service worker (MV3): 侧边栏行为 + LLM代理 + API兜底 + 评论历史 + 运行编排
|
||||
|
||||
// 点工具栏图标 → 打开侧边栏(不要设 default_popup,会被 popup 抢走)
|
||||
if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
||||
}
|
||||
|
||||
// B站 CDN(bilivideo.com)要求 Referer 才给视频文件,而 chrome.downloads 不能自定义
|
||||
// Referer(Unsafe header)→ 用 declarativeNetRequest 给这些请求自动注入 Referer
|
||||
async function ensureDownloadHeadersRule() {
|
||||
try {
|
||||
const existing = await chrome.declarativeNetRequest.getDynamicRules();
|
||||
const removeRuleIds = existing.filter(r => r.id === 1001).map(r => r.id);
|
||||
await chrome.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: removeRuleIds,
|
||||
addRules: [{
|
||||
id: 1001,
|
||||
priority: 1,
|
||||
action: {
|
||||
type: "modifyHeaders",
|
||||
requestHeaders: [{ header: "Referer", operation: "set", value: "https://www.bilibili.com/" }]
|
||||
},
|
||||
condition: {
|
||||
requestDomains: ["bilivideo.com", "hdslb.com"]
|
||||
}
|
||||
}]
|
||||
});
|
||||
} catch (e) { /* 权限或浏览器版本不支持时忽略,下载会403并有报错提示 */ }
|
||||
}
|
||||
ensureDownloadHeadersRule();
|
||||
|
||||
// ============ 授权(CardAuth: auth.88531.cn,项目=B站评论插件) ============
|
||||
|
||||
const AUTH_BASE = "https://auth.88531.cn/api";
|
||||
const PROJECT_NAME = "B站评论插件";
|
||||
const PROJECT_ID = 4;
|
||||
const TRIAL_MS = 24 * 3600 * 1000; // 未授权试用1天
|
||||
const CARD_API_KEY = "ak_ab99ed40b2296d6d2db06a4b5692b987";
|
||||
|
||||
async function getMachineId() {
|
||||
const c = await chrome.storage.local.get("machineId");
|
||||
if (c.machineId && /^\d{10,16}$/.test(String(c.machineId))) return String(c.machineId);
|
||||
let id = "";
|
||||
for (let i = 0; i < 13; i++) {
|
||||
id += (i === 0 ? String(1 + Math.floor(Math.random() * 9)) : String(Math.floor(Math.random() * 10)));
|
||||
}
|
||||
await chrome.storage.local.set({ machineId: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
async function authFetch(path, opts, timeoutMs) {
|
||||
const ctl = new AbortController();
|
||||
const timer = setTimeout(() => ctl.abort(), timeoutMs || 8000);
|
||||
try {
|
||||
const resp = await fetch(AUTH_BASE + path, Object.assign({ signal: ctl.signal }, opts || {}));
|
||||
return await resp.json();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// 授权状态:机器码在 CardAuth 是否有本项目的有效授权;无授权则看本地1天试用
|
||||
// 授权服务器不可用时 fail-open(放行),绝不因服务器故障锁死用户
|
||||
async function checkLicense() {
|
||||
const machine = await getMachineId();
|
||||
let authorized = false;
|
||||
let expire = "";
|
||||
let offline = false;
|
||||
try {
|
||||
const r = await authFetch("/public/authorizations/query?bot_qq=" + machine);
|
||||
if (r && r.code === 200 && r.data && Array.isArray(r.data.list)) {
|
||||
// 只认本项目的授权(其它项目的卡不能解锁本插件)
|
||||
const mine = r.data.list.filter(it =>
|
||||
it && it.project_name === PROJECT_NAME && it.status === "active" && !it.is_expired);
|
||||
if (mine.length) {
|
||||
authorized = true;
|
||||
const times = mine.map(it => it.expire_time).filter(Boolean).sort();
|
||||
expire = times.length ? times[times.length - 1] : "";
|
||||
}
|
||||
} else if (r && r.code === 200 && r.data && r.data.has_valid_auth === false) {
|
||||
authorized = false;
|
||||
} else if (r && r.code === 200) {
|
||||
// 形状异常时按 fail-safe 处理为未授权(有试用兑底),但网络通
|
||||
authorized = false;
|
||||
} else {
|
||||
offline = true;
|
||||
}
|
||||
} catch (e) {
|
||||
offline = true; // 网络/超时 → 放行
|
||||
}
|
||||
|
||||
const st = await chrome.storage.local.get("trialStart");
|
||||
let trialStart = Number(st.trialStart) || 0;
|
||||
if (!trialStart) {
|
||||
trialStart = Date.now();
|
||||
await chrome.storage.local.set({ trialStart: trialStart });
|
||||
}
|
||||
const trialEnd = trialStart + TRIAL_MS;
|
||||
const leftMs = Math.max(0, trialEnd - Date.now());
|
||||
|
||||
return {
|
||||
authorized: authorized || offline, // offline 时视为放行
|
||||
offline: offline,
|
||||
realAuthorized: authorized,
|
||||
expire: expire,
|
||||
machine: machine,
|
||||
trialStart: trialStart,
|
||||
trialEnd: trialEnd,
|
||||
trialLeftMs: leftMs
|
||||
};
|
||||
}
|
||||
|
||||
// 卡密激活:verify 首次调用即绑定机器码(客户端式激活)
|
||||
async function activateCard(cardKey) {
|
||||
const machine = await getMachineId();
|
||||
const r = await authFetch("/public/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Api-Key": CARD_API_KEY },
|
||||
body: JSON.stringify({
|
||||
card_key: String(cardKey || "").trim(),
|
||||
machine_id: machine,
|
||||
client_version: chrome.runtime.getManifest().version
|
||||
})
|
||||
}, 12000);
|
||||
const d = (r && r.data) || {};
|
||||
if (r && r.code === 200 && d.valid) {
|
||||
return { ok: true, message: d.message || "激活成功", days: d.duration_days || 0 };
|
||||
}
|
||||
return { ok: false, message: (d && d.message) || (r && r.message) || "激活失败" };
|
||||
}
|
||||
|
||||
// 公告:只取本项目发布的(后台公告管理里选「B站评论插件」项目)
|
||||
async function getAnnouncements() {
|
||||
const r = await authFetch("/public/announcements", {}, 8000);
|
||||
const list = (r && r.code === 200 && r.data && Array.isArray(r.data.list)) ? r.data.list : [];
|
||||
return list
|
||||
.filter(a => a && Number(a.project_id) === PROJECT_ID)
|
||||
.sort((a, b) => (b.is_top || 0) - (a.is_top || 0))
|
||||
.slice(0, 3)
|
||||
.map(a => ({ title: a.title || "", content: a.content || "", link: a.link_url || "" }));
|
||||
}
|
||||
|
||||
// ============ LLM(OpenAI 兼容接口) ============
|
||||
|
||||
// 归一化 Base URL:去尾斜杠;已带 /chat/completions 直接用;路径无 /vN 段则补 /v1
|
||||
function normalizeChatUrl(raw) {
|
||||
let s = String(raw || "").trim().replace(/\/+$/, "");
|
||||
if (!s) return "";
|
||||
if (/\/chat\/completions$/.test(s)) return s;
|
||||
if (!/\/v\d+(\/|$)/.test(s)) s += "/v1";
|
||||
return s + "/chat/completions";
|
||||
}
|
||||
|
||||
// 从各种返回形状取正文(绝不回退 reasoning_content)
|
||||
function extractContent(data) {
|
||||
if (!data) return "";
|
||||
const c = data.choices && data.choices[0];
|
||||
if (c) {
|
||||
const m = c.message;
|
||||
if (m) {
|
||||
if (typeof m.content === "string" && m.content.trim()) return m.content;
|
||||
if (Array.isArray(m.content)) {
|
||||
const t = m.content.map(p => (p && (p.text || p.content)) || "").join("");
|
||||
if (t.trim()) return t;
|
||||
}
|
||||
}
|
||||
if (typeof c.text === "string" && c.text.trim()) return c.text;
|
||||
}
|
||||
if (typeof data.output_text === "string" && data.output_text.trim()) return data.output_text;
|
||||
if (typeof data.output === "string" && data.output.trim()) return data.output;
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildBody(p) {
|
||||
const body = {
|
||||
model: p.model,
|
||||
messages: [
|
||||
{ role: "system", content: p.system || "你只输出结果文本本身,不做任何解释。" },
|
||||
{ role: "user", content: p.prompt }
|
||||
],
|
||||
temperature: typeof p.temperature === "number" ? p.temperature : 1.0,
|
||||
max_tokens: p.maxTokens || 800,
|
||||
presence_penalty: 0.3,
|
||||
frequency_penalty: 0.4,
|
||||
thinking: { type: "disabled" }
|
||||
};
|
||||
return body;
|
||||
}
|
||||
|
||||
async function doChat(url, key, body, timeoutMs) {
|
||||
const ctl = new AbortController();
|
||||
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + (key || "")
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: ctl.signal
|
||||
});
|
||||
const text = await resp.text();
|
||||
let data = null;
|
||||
try { data = JSON.parse(text); } catch (e) { data = null; }
|
||||
return { resp, data, text };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// 400/422 修复阶梯:最多一次重试
|
||||
function repairBody(body, data, status) {
|
||||
const msg = (((data || {}).error || {}).message || "").toString();
|
||||
const lower = msg.toLowerCase();
|
||||
if (/thinking|unknown|unrecognized|extra|invalid|not allowed|parameter|不支持|参数/.test(lower)) {
|
||||
if (body.thinking) {
|
||||
const nb = Object.assign({}, body);
|
||||
delete nb.thinking;
|
||||
return { body: nb, reason: "去掉 thinking 字段" };
|
||||
}
|
||||
if (body.max_tokens && /max_tokens|max_completion_tokens/.test(lower)) {
|
||||
const nb = Object.assign({}, body);
|
||||
nb.max_completion_tokens = nb.max_tokens;
|
||||
delete nb.max_tokens;
|
||||
return { body: nb, reason: "max_tokens 改名 max_completion_tokens" };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleCallAiApi(p, sendResponse) {
|
||||
const url = normalizeChatUrl(p.baseUrl);
|
||||
if (!url || !p.model) {
|
||||
sendResponse({ ok: false, error: "模型未配置(缺 Base URL 或模型名)" });
|
||||
return;
|
||||
}
|
||||
const timeoutMs = p.timeoutMs || 90000;
|
||||
const key = p.apiKey || "";
|
||||
let body = buildBody(p);
|
||||
|
||||
let r;
|
||||
try {
|
||||
r = await doChat(url, key, body, timeoutMs);
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: String(e && e.message || e).includes("abort") ? "请求超时(" + Math.round(timeoutMs / 1000) + "秒)" : String(e) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (r.resp.status === 400 || r.resp.status === 422) {
|
||||
const fixed = repairBody(body, r.data, r.resp.status);
|
||||
if (fixed) {
|
||||
try {
|
||||
r = await doChat(url, key, fixed.body, timeoutMs);
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: "请求超时(" + Math.round(timeoutMs / 1000) + "秒)" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!r.resp.ok) {
|
||||
const msg = (r.data && r.data.error && r.data.error.message) || ("HTTP " + r.resp.status);
|
||||
sendResponse({ ok: false, error: String(msg) });
|
||||
return;
|
||||
}
|
||||
|
||||
let content = extractContent(r.data);
|
||||
const finish = r.data && r.data.choices && r.data.choices[0] && r.data.choices[0].finish_reason;
|
||||
const hasReasoning = !!(r.data && r.data.choices && r.data.choices[0] && r.data.choices[0].message && r.data.choices[0].message.reasoning_content);
|
||||
|
||||
if (!content && (finish === "length" || hasReasoning)) {
|
||||
// 思考模型吃掉了 max_tokens → 提额重试一次
|
||||
const nb = Object.assign({}, body, { max_tokens: Math.max(4096, (body.max_tokens || 800) * 4) });
|
||||
try {
|
||||
const r2 = await doChat(url, key, nb, timeoutMs);
|
||||
if (r2.resp.ok) content = extractContent(r2.data);
|
||||
} catch (e) { /* 保持 content 为空走下方报错 */ }
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
sendResponse({ ok: false, error: "模型只返回了思考过程,没有返回正文(可提高 max_tokens 或换非思考型模型)" });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: true, content: content.trim() });
|
||||
}
|
||||
|
||||
// ============ API 兜底代理(页面 fetch 被 CORS/CSP 拦时走这里) ============
|
||||
|
||||
async function handleBgFetch(p, sendResponse) {
|
||||
const ctl = new AbortController();
|
||||
const timer = setTimeout(() => ctl.abort(), p.timeoutMs || 20000);
|
||||
try {
|
||||
const resp = await fetch(p.url, {
|
||||
method: p.method || "GET",
|
||||
headers: p.headers || {},
|
||||
body: p.body,
|
||||
credentials: "include",
|
||||
signal: ctl.signal
|
||||
});
|
||||
const text = await resp.text();
|
||||
sendResponse({ ok: true, status: resp.status, text });
|
||||
} catch (e) {
|
||||
const aborted = String(e && e.name) === "AbortError";
|
||||
sendResponse({ ok: false, error: aborted ? "请求超时(" + Math.round((p.timeoutMs || 20000) / 1000) + "秒)" : String(e) });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 评论历史(去重 + 统计) ============
|
||||
|
||||
async function getHistory() {
|
||||
const c = await chrome.storage.local.get("commentHistory");
|
||||
const h = c.commentHistory;
|
||||
return Array.isArray(h) ? h : [];
|
||||
}
|
||||
|
||||
async function addRecord(rec) {
|
||||
const h = await getHistory();
|
||||
if (h.some(x => x.bvid === rec.bvid)) return h;
|
||||
h.unshift({ bvid: rec.bvid, title: rec.title || "", text: rec.text || "", keyword: rec.keyword || "", ts: rec.ts || Date.now() });
|
||||
if (h.length > 3000) h.length = 3000;
|
||||
await chrome.storage.local.set({ commentHistory: h });
|
||||
return h;
|
||||
}
|
||||
|
||||
function statsOf(h) {
|
||||
const now = Date.now();
|
||||
const DAY = 86400000;
|
||||
const d = { total: h.length, today: 0, week: 0, list: h };
|
||||
for (const r of h) {
|
||||
const age = now - (r.ts || 0);
|
||||
if (age <= DAY) d.today++;
|
||||
if (age <= 7 * DAY) d.week++;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
// ============ 运行编排:任务存后台内存(跨子域名页面共享),页面加载后主动领任务 ============
|
||||
|
||||
let ACTIVE_TASK = null; // 当前未完成任务(唯一权威副本)
|
||||
|
||||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
|
||||
function waitForTabComplete(tabId, timeoutMs) {
|
||||
return new Promise(resolve => {
|
||||
let done = false;
|
||||
const onUpd = (id, info) => {
|
||||
if (id === tabId && info.status === "complete" && !done) {
|
||||
done = true;
|
||||
try { chrome.tabs.onUpdated.removeListener(onUpd); } catch (e) {}
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(onUpd);
|
||||
setTimeout(() => {
|
||||
if (!done) { done = true; try { chrome.tabs.onUpdated.removeListener(onUpd); } catch (e) {} resolve(false); }
|
||||
}, timeoutMs || 10000);
|
||||
});
|
||||
}
|
||||
|
||||
// 内容脚本版本(老脚本没有 ver 处理会抛错 → 返回空串)
|
||||
async function contentVersion(tabId) {
|
||||
try {
|
||||
const r = await chrome.tabs.sendMessage(tabId, { action: "ver" });
|
||||
return (r && r.ver) || "";
|
||||
} catch (e) { return ""; }
|
||||
}
|
||||
|
||||
// 版本握手后确保页面里是最新脚本(不一致则刷新页面),返回是否就绪
|
||||
async function ensureFreshContent(tabId) {
|
||||
const want = chrome.runtime.getManifest().version;
|
||||
let ver = await contentVersion(tabId);
|
||||
if (ver === want) return true;
|
||||
await chrome.tabs.reload(tabId);
|
||||
await waitForTabComplete(tabId, 10000);
|
||||
for (let i = 0; i < 12 && ver !== want; i++) {
|
||||
await sleep(500);
|
||||
ver = await contentVersion(tabId);
|
||||
}
|
||||
return ver === want;
|
||||
}
|
||||
|
||||
async function findBiliTab() {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
// 优先匹配任务当前目标视频所在的标签页(停止指令要发到正在执行的那一页)
|
||||
if (ACTIVE_TASK && Array.isArray(ACTIVE_TASK.queue) && ACTIVE_TASK.queue[ACTIVE_TASK.index]) {
|
||||
const bvid = ACTIVE_TASK.queue[ACTIVE_TASK.index].bvid;
|
||||
const hit = tabs.find(t => t.url && t.url.indexOf(bvid) >= 0);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return tabs.find(t => t.url && /^https:\/\/[^/]*bilibili\.com\//.test(t.url)) || null;
|
||||
}
|
||||
|
||||
async function ensureContent(tabId) {
|
||||
// 自愈注入:manifest 的 content_scripts 只在页面加载后存在,新装扩展的旧标签页没有
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId }, files: ["content.js"] });
|
||||
} catch (e) { /* 已存在或页面不允许注入 */ }
|
||||
}
|
||||
|
||||
async function sendWithRetry(tabId, msg, tries) {
|
||||
let lastErr = null;
|
||||
for (let i = 0; i < (tries || 10); i++) {
|
||||
try {
|
||||
return await chrome.tabs.sendMessage(tabId, msg);
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
throw lastErr || new Error("消息发送失败");
|
||||
}
|
||||
|
||||
async function handleStartRun(p, sendResponse) {
|
||||
try {
|
||||
// 授权闸门:未授权且试用到期 → 拦截自动评论(下载视频不走这里,不受影响)
|
||||
const lic = await checkLicense();
|
||||
if (!lic.authorized && lic.trialLeftMs <= 0) {
|
||||
sendResponse({ ok: false, error: "免费试用已到期,自动评论已锁定。购买卡密激活后即可继续使用(下载视频不受影响)" });
|
||||
return;
|
||||
}
|
||||
ACTIVE_TASK = null; // 本次点「开始」= 全新任务,作废残留的旧任务(旧任务会带着预生成的旧文案继续跑)
|
||||
let tab = await findBiliTab();
|
||||
if (!tab) {
|
||||
tab = await chrome.tabs.create({
|
||||
url: "https://search.bilibili.com/all?keyword=" + encodeURIComponent(p.config.keyword || "")
|
||||
});
|
||||
await sleep(1800); // 等页面加载出内容脚本
|
||||
} else {
|
||||
await chrome.tabs.update(tab.id, { active: true });
|
||||
}
|
||||
await ensureContent(tab.id);
|
||||
|
||||
// 版本握手:重载扩展不会热更新已打开页面里的老脚本 → 不一致先刷新页面换新脚本
|
||||
const fresh = await ensureFreshContent(tab.id);
|
||||
if (!fresh) {
|
||||
sendResponse({ ok: false, error: "B站页面脚本未就绪,请手动刷新该标签页后重试" });
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await sendWithRetry(tab.id, { action: "start", config: p.config });
|
||||
sendResponse({ ok: true, tabId: tab.id, res: res || null });
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: "无法连接B站页面:" + String(e && e.message || e) + "(请刷新B站标签页后重试)" });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStopRun(p, sendResponse) {
|
||||
try {
|
||||
const tab = await findBiliTab();
|
||||
if (!tab) { sendResponse({ ok: false, error: "没找到B站标签页" }); return; }
|
||||
await ensureContent(tab.id);
|
||||
const res = await sendWithRetry(tab.id, { action: "stop" }, 4);
|
||||
sendResponse({ ok: true, res: res || null });
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: String(e && e.message || e) });
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 下载当前页视频(取直链,实际抓取与保存在侧边栏页面完成) ============
|
||||
|
||||
async function handleGetDownloadInfo(p, sendResponse) {
|
||||
try {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const videoTabs = tabs.filter(t => t.url && /bilibili\.com\/video\//.test(t.url));
|
||||
if (!videoTabs.length) {
|
||||
sendResponse({ ok: false, error: "请先打开一个B站视频页,再点下载" });
|
||||
return;
|
||||
}
|
||||
// 优先当前活动的视频页
|
||||
videoTabs.sort((a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0));
|
||||
const tab = videoTabs[0];
|
||||
await ensureContent(tab.id);
|
||||
const fresh = await ensureFreshContent(tab.id);
|
||||
if (!fresh) { sendResponse({ ok: false, error: "B站页面脚本未就绪,请刷新该视频页后重试" }); return; }
|
||||
|
||||
const r = await sendWithRetry(tab.id, { action: "getDownload", qn: Number(p.qn) || 80 });
|
||||
if (!r || !r.ok || !r.info) {
|
||||
sendResponse({ ok: false, error: (r && r.error) || "页面无响应" });
|
||||
return;
|
||||
}
|
||||
const info = r.info;
|
||||
if (!Array.isArray(info.files) || !info.files.length) {
|
||||
sendResponse({ ok: false, error: "没有可下载的地址" });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: true, info: info });
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: String(e && e.message || e) });
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 消息路由 ============
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (!msg) return;
|
||||
|
||||
// 内容脚本的进度事件:侧边栏自身也会直接收到(runtime 消息发给除发送者外的所有扩展上下文),
|
||||
// 这里不再转发,否则面板收到两份 → 成功列表/日志显示重复
|
||||
if (msg.relay) {
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.action === "callAiApi") {
|
||||
handleCallAiApi(msg, sendResponse);
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "bgFetch") {
|
||||
handleBgFetch(msg, sendResponse);
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "licenseStatus") {
|
||||
(async () => {
|
||||
const license = await checkLicense();
|
||||
let announcements = [];
|
||||
try { announcements = await getAnnouncements(); } catch (e) { announcements = []; }
|
||||
sendResponse({ ok: true, license: license, announcements: announcements });
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "activateCard") {
|
||||
(async () => {
|
||||
try { sendResponse(await activateCard(msg.card)); }
|
||||
catch (e) { sendResponse({ ok: false, error: String(e && e.message || e) }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "setTask") {
|
||||
ACTIVE_TASK = msg.task || null;
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
if (msg.action === "pullTask") {
|
||||
// 只给新鲜任务(120秒内更新过),防止关掉标签页后残留任务在别的页面乱续
|
||||
if (ACTIVE_TASK && Date.now() - (ACTIVE_TASK.ts || 0) <= 120000) {
|
||||
sendResponse({ task: ACTIVE_TASK });
|
||||
} else {
|
||||
ACTIVE_TASK = null;
|
||||
sendResponse({ task: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.action === "clearTask") {
|
||||
ACTIVE_TASK = null;
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
if (msg.action === "startRun") {
|
||||
handleStartRun(msg, sendResponse);
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "stopRun") {
|
||||
handleStopRun(msg, sendResponse);
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "getDownloadInfo") {
|
||||
handleGetDownloadInfo(msg, sendResponse);
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "addRecord") {
|
||||
(async () => {
|
||||
const h = await addRecord({ bvid: msg.bvid, title: msg.title, text: msg.text, keyword: msg.keyword, ts: msg.ts });
|
||||
const s = statsOf(h);
|
||||
sendResponse({ ok: true, stats: { total: s.total, today: s.today, week: s.week } });
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "getStats") {
|
||||
(async () => {
|
||||
const s = statsOf(await getHistory());
|
||||
sendResponse({ stats: { total: s.total, today: s.today, week: s.week }, list: s.list.slice(0, 100) });
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.action === "clearHistory") {
|
||||
(async () => {
|
||||
await chrome.storage.local.set({ commentHistory: [] });
|
||||
sendResponse({ ok: true, stats: { total: 0, today: 0, week: 0 } });
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user