489 lines
15 KiB
JavaScript
489 lines
15 KiB
JavaScript
|
|
const DEFAULT_CONFIG = {
|
|||
|
|
serverUrl: "http://localhost:3000",
|
|||
|
|
shopId: "",
|
|||
|
|
pollInterval: 20,
|
|||
|
|
dailyLimit: 20,
|
|||
|
|
publishIntervalMin: 30,
|
|||
|
|
publishIntervalMax: 90,
|
|||
|
|
maxConsecutiveFailures: 3,
|
|||
|
|
enabled: false,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let state = {
|
|||
|
|
polling: false,
|
|||
|
|
timerId: null,
|
|||
|
|
todayCount: 0,
|
|||
|
|
consecutiveFailures: 0,
|
|||
|
|
lastPublishTime: 0,
|
|||
|
|
pendingCount: 0,
|
|||
|
|
recentResults: [],
|
|||
|
|
rapidFailPaused: false,
|
|||
|
|
statusError: "",
|
|||
|
|
runNextImmediately: false,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Load state from storage
|
|||
|
|
async function loadState() {
|
|||
|
|
const data = await chrome.storage.local.get(["todayCount", "consecutiveFailures", "todayDate"]);
|
|||
|
|
const today = new Date().toISOString().slice(0, 10);
|
|||
|
|
if (data.todayDate !== today) {
|
|||
|
|
state.todayCount = 0;
|
|||
|
|
state.consecutiveFailures = 0;
|
|||
|
|
} else {
|
|||
|
|
state.todayCount = data.todayCount || 0;
|
|||
|
|
state.consecutiveFailures = data.consecutiveFailures || 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveState() {
|
|||
|
|
const today = new Date().toISOString().slice(0, 10);
|
|||
|
|
await chrome.storage.local.set({
|
|||
|
|
todayCount: state.todayCount,
|
|||
|
|
consecutiveFailures: state.consecutiveFailures,
|
|||
|
|
todayDate: today,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function getConfig() {
|
|||
|
|
const data = await chrome.storage.local.get(["config"]);
|
|||
|
|
return { ...DEFAULT_CONFIG, ...(data.config || {}) };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function setConfig(config) {
|
|||
|
|
await chrome.storage.local.set({ config });
|
|||
|
|
return config;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function resolveShopId(shopInput) {
|
|||
|
|
const q = String(shopInput || "").trim();
|
|||
|
|
if (!q) throw new Error("未配置店铺ID");
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const resolved = await apiFetch(`/api/shops/resolve?q=${encodeURIComponent(q)}`);
|
|||
|
|
if (resolved && resolved.id) return resolved.id;
|
|||
|
|
} catch (e) {
|
|||
|
|
throw new Error(`未找到店铺:${q}。请确认输入的是系统中的店铺ID/店铺号/店铺名称`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw new Error(`未找到店铺:${q}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fetch with error handling
|
|||
|
|
async function apiFetch(path, opts = {}) {
|
|||
|
|
const config = await getConfig();
|
|||
|
|
const url = `${config.serverUrl}${path}`;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(url, {
|
|||
|
|
headers: { "Content-Type": "application/json", ...opts.headers },
|
|||
|
|
...opts,
|
|||
|
|
});
|
|||
|
|
if (!res.ok) {
|
|||
|
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|||
|
|
throw new Error(err.error || `HTTP ${res.status}`);
|
|||
|
|
}
|
|||
|
|
return res.json();
|
|||
|
|
} catch (e) {
|
|||
|
|
throw new Error(`网络错误: ${e.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getWorkerId() {
|
|||
|
|
return `extension-${chrome.runtime.id.slice(0, 8)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Claim next task from server
|
|||
|
|
async function claimTask(resolvedShopId) {
|
|||
|
|
try {
|
|||
|
|
const data = await apiFetch(
|
|||
|
|
`/api/tasks/claim`,
|
|||
|
|
{
|
|||
|
|
method: "POST",
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
shop_id: resolvedShopId,
|
|||
|
|
claimed_by: getWorkerId(),
|
|||
|
|
}),
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
return data;
|
|||
|
|
} catch (e) {
|
|||
|
|
// "No pending tasks" is normal, not an error
|
|||
|
|
if (e.message.includes("No pending tasks") || e.message.includes("404")) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
throw e;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Report task result
|
|||
|
|
async function reportComplete(taskId, noteId, result = {}) {
|
|||
|
|
await apiFetch(`/api/tasks/${taskId}/complete`, {
|
|||
|
|
method: "POST",
|
|||
|
|
body: JSON.stringify({ result, claimed_by: getWorkerId() }),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function reportFail(taskId, noteId, errorMessage) {
|
|||
|
|
await apiFetch(`/api/tasks/${taskId}/fail`, {
|
|||
|
|
method: "POST",
|
|||
|
|
body: JSON.stringify({ error_message: errorMessage, claimed_by: getWorkerId() }),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Random delay between min and max seconds
|
|||
|
|
function randomDelayMs(minSec, maxSec) {
|
|||
|
|
return Math.floor(Math.random() * (maxSec - minSec + 1) + minSec) * 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getItemSelectUrl() {
|
|||
|
|
return "https://ark.xiaohongshu.com/app-note/publish-item-select?source=noteManagement-GoodsList¬eFrom=NOTE_TAB&step=itemSelect";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function waitForTabComplete(tabId, timeoutMs = 20000) {
|
|||
|
|
const startedAt = Date.now();
|
|||
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|||
|
|
const tab = await chrome.tabs.get(tabId);
|
|||
|
|
if (tab.status === "complete") return tab;
|
|||
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|||
|
|
}
|
|||
|
|
return chrome.tabs.get(tabId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function ensureQianfanTab() {
|
|||
|
|
const tabs = await chrome.tabs.query({ url: "https://ark.xiaohongshu.com/*" });
|
|||
|
|
const publishTab = tabs.find((tab) => {
|
|||
|
|
const url = tab.url || "";
|
|||
|
|
return url.includes("publish-item-select") || url.includes("/app-note/publish");
|
|||
|
|
});
|
|||
|
|
if (publishTab) return publishTab;
|
|||
|
|
|
|||
|
|
console.log("[千帆助手] 未找到千帆发布页,自动打开选品页");
|
|||
|
|
const tab = await chrome.tabs.create({ url: getItemSelectUrl(), active: true });
|
|||
|
|
return waitForTabComplete(tab.id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Main publish loop
|
|||
|
|
async function publishLoop() {
|
|||
|
|
const config = await getConfig();
|
|||
|
|
if (!config.enabled || !config.shopId) {
|
|||
|
|
state.polling = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check daily limit
|
|||
|
|
if (state.todayCount >= config.dailyLimit) {
|
|||
|
|
console.log(`[千帆助手] 今日已发布 ${state.todayCount} 条,达到上限 ${config.dailyLimit}`);
|
|||
|
|
updateBadge("达限", "#f59e0b");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let resolvedShopId;
|
|||
|
|
try {
|
|||
|
|
resolvedShopId = await resolveShopId(config.shopId);
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error("[千帆助手] 店铺解析失败:", e.message);
|
|||
|
|
notify("店铺未找到", e.message);
|
|||
|
|
updateBadge("店铺错", "#ef4444");
|
|||
|
|
scheduleNext();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 先确保发布/选品页可用,再领取 task,避免任务被领走后没有页面可执行。
|
|||
|
|
const qianfanTab = await ensureQianfanTab();
|
|||
|
|
const tabs = [qianfanTab];
|
|||
|
|
|
|||
|
|
let taskData = null;
|
|||
|
|
try {
|
|||
|
|
taskData = await claimTask(resolvedShopId);
|
|||
|
|
if (!taskData || !taskData.note) {
|
|||
|
|
console.log("[千帆助手] 暂无待发布任务");
|
|||
|
|
updateBadge("等待", "#6b7280");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`[千帆助手] 领取任务: ${taskData.note.title}`);
|
|||
|
|
updateBadge("发布中", "#3b82f6");
|
|||
|
|
|
|||
|
|
// Find a tab that is on the千帆 page (item-select or publish)
|
|||
|
|
let targetTab = null;
|
|||
|
|
for (const tab of tabs) {
|
|||
|
|
try {
|
|||
|
|
const pingResult = await chrome.tabs.sendMessage(tab.id, { type: "PING" });
|
|||
|
|
if (pingResult && (pingResult.isPublishPage || pingResult.isItemSelectPage)) {
|
|||
|
|
targetTab = tab;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
// Content script not loaded on this tab, skip
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!targetTab) {
|
|||
|
|
// content script 未注入(页面在插件加载前已打开),主动注入后重试 PING
|
|||
|
|
console.log("[千帆助手] content script 未响应,尝试主动注入");
|
|||
|
|
for (const tab of tabs) {
|
|||
|
|
try {
|
|||
|
|
await chrome.scripting.executeScript({
|
|||
|
|
target: { tabId: tab.id },
|
|||
|
|
files: ["content_scripts/qianfan_publish.js"],
|
|||
|
|
});
|
|||
|
|
const pingResult = await chrome.tabs.sendMessage(tab.id, { type: "PING" });
|
|||
|
|
if (pingResult && (pingResult.isPublishPage || pingResult.isItemSelectPage)) {
|
|||
|
|
targetTab = tab;
|
|||
|
|
console.log("[千帆助手] 主动注入后 content script 已就绪");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!targetTab) {
|
|||
|
|
throw new Error("千帆页内容脚本未加载,请刷新千帆页(Ctrl+R)后重试");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Execute publish
|
|||
|
|
const response = await chrome.tabs.sendMessage(targetTab.id, {
|
|||
|
|
type: "EXECUTE_PUBLISH",
|
|||
|
|
task: taskData,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response && response.success) {
|
|||
|
|
await reportComplete(taskData.task_id, taskData.note.id, response.result);
|
|||
|
|
if (response.result && response.result.continuedToItemSelect === false) {
|
|||
|
|
console.warn("[千帆助手] 发布成功后未确认回到选品页,主动导航回选品页");
|
|||
|
|
await chrome.tabs.update(targetTab.id, { url: getItemSelectUrl() });
|
|||
|
|
await waitForTabComplete(targetTab.id);
|
|||
|
|
}
|
|||
|
|
state.todayCount++;
|
|||
|
|
state.consecutiveFailures = 0;
|
|||
|
|
state.recentResults = [];
|
|||
|
|
state.runNextImmediately = true;
|
|||
|
|
await saveState();
|
|||
|
|
console.log(`[千帆助手] 发布成功: ${taskData.note.title}`);
|
|||
|
|
updateBadge("续发", "#22c55e");
|
|||
|
|
} else {
|
|||
|
|
throw new Error(response?.error || "发布执行失败");
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(`[千帆助手] 任务失败:`, e.message, e.stack);
|
|||
|
|
state.consecutiveFailures++;
|
|||
|
|
await saveState();
|
|||
|
|
|
|||
|
|
if (taskData) {
|
|||
|
|
try {
|
|||
|
|
await reportFail(taskData.task_id, taskData.note.id, e.message);
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateBadge("失败", "#ef4444");
|
|||
|
|
|
|||
|
|
state.recentResults.push({ success: false, ts: Date.now() });
|
|||
|
|
state.recentResults = state.recentResults.slice(-5);
|
|||
|
|
if (state.recentResults.length >= 5) {
|
|||
|
|
const elapsed = state.recentResults[4].ts - state.recentResults[0].ts;
|
|||
|
|
const allFail = state.recentResults.every((r) => !r.success);
|
|||
|
|
if (allFail && elapsed < 60000) {
|
|||
|
|
console.warn(`[千帆助手] 检测到连续快速失败(${elapsed}ms内5次),已暂停`);
|
|||
|
|
state.rapidFailPaused = true;
|
|||
|
|
state.polling = false;
|
|||
|
|
notify("已自动暂停", "检测到连续5次快速失败(60秒内),请检查千帆页面后重置计数再启动");
|
|||
|
|
updateBadge("暂停", "#ef4444");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
scheduleNext();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function scheduleNext() {
|
|||
|
|
if (state.polling) {
|
|||
|
|
getConfig().then((cfg) => {
|
|||
|
|
const delay = state.runNextImmediately ? 1000 : randomDelayMs(cfg.publishIntervalMin, cfg.publishIntervalMax);
|
|||
|
|
state.runNextImmediately = false;
|
|||
|
|
if (state.timerId) clearTimeout(state.timerId);
|
|||
|
|
state.timerId = setTimeout(publishLoop, delay);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateBadge(text, color) {
|
|||
|
|
chrome.action.setBadgeText({ text });
|
|||
|
|
chrome.action.setBadgeBackgroundColor({ color });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function notify(title, message) {
|
|||
|
|
chrome.notifications.create({
|
|||
|
|
type: "basic",
|
|||
|
|
iconUrl: "icons/icon128.png",
|
|||
|
|
title: `千帆助手 - ${title}`,
|
|||
|
|
message,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start/stop polling
|
|||
|
|
async function startPolling() {
|
|||
|
|
if (state.polling) return;
|
|||
|
|
state.polling = true;
|
|||
|
|
state.rapidFailPaused = false;
|
|||
|
|
// Persist enabled state so it survives service worker restart
|
|||
|
|
const config = await getConfig();
|
|||
|
|
await chrome.storage.local.set({ config: { ...config, enabled: true } });
|
|||
|
|
await loadState();
|
|||
|
|
console.log("[千帆助手] 开始轮询发布");
|
|||
|
|
updateBadge("运行", "#22c55e");
|
|||
|
|
publishLoop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function stopPolling() {
|
|||
|
|
state.polling = false;
|
|||
|
|
if (state.timerId) {
|
|||
|
|
clearTimeout(state.timerId);
|
|||
|
|
state.timerId = null;
|
|||
|
|
}
|
|||
|
|
// Persist disabled state
|
|||
|
|
const config = await getConfig();
|
|||
|
|
await chrome.storage.local.set({ config: { ...config, enabled: false } });
|
|||
|
|
console.log("[千帆助手] 停止轮询");
|
|||
|
|
updateBadge("停止", "#6b7280");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Message handler
|
|||
|
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||
|
|
if (msg.type === "GET_STATUS") {
|
|||
|
|
loadState().then(async () => {
|
|||
|
|
try {
|
|||
|
|
const config = await getConfig();
|
|||
|
|
if (config.shopId) {
|
|||
|
|
const shopId = await resolveShopId(config.shopId);
|
|||
|
|
const notes = await apiFetch(`/api/notes?shop_id=${shopId}&status=approved`);
|
|||
|
|
state.pendingCount = Array.isArray(notes) ? notes.length : 0;
|
|||
|
|
state.statusError = "";
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
state.pendingCount = 0;
|
|||
|
|
state.statusError = e.message || "状态查询失败";
|
|||
|
|
}
|
|||
|
|
sendResponse({
|
|||
|
|
polling: state.polling,
|
|||
|
|
todayCount: state.todayCount,
|
|||
|
|
consecutiveFailures: state.consecutiveFailures,
|
|||
|
|
pendingCount: state.pendingCount,
|
|||
|
|
statusError: state.statusError || "",
|
|||
|
|
});
|
|||
|
|
}).catch(() => {
|
|||
|
|
sendResponse({
|
|||
|
|
polling: state.polling,
|
|||
|
|
todayCount: state.todayCount,
|
|||
|
|
consecutiveFailures: state.consecutiveFailures,
|
|||
|
|
pendingCount: 0,
|
|||
|
|
statusError: state.statusError || "",
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.type === "START_POLLING") {
|
|||
|
|
// Fire and forget - start polling without blocking the message channel
|
|||
|
|
startPolling().catch(e => console.error("[千帆助手] 启动失败:", e.message));
|
|||
|
|
// Respond immediately
|
|||
|
|
sendResponse({ ok: true, polling: true });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.type === "STOP_POLLING") {
|
|||
|
|
stopPolling().then(() => sendResponse({ ok: true, polling: false }));
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.type === "SET_CONFIG") {
|
|||
|
|
setConfig(msg.config).then(() => sendResponse({ ok: true })).catch(() => sendResponse({ ok: false }));
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.type === "GET_CONFIG") {
|
|||
|
|
getConfig().then((c) => sendResponse(c)).catch(() => sendResponse({ ...DEFAULT_CONFIG }));
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.type === "SELECTOR_FAILURE") {
|
|||
|
|
console.error("[千帆助手] 选择器失效告警:", msg.step, msg.selector);
|
|||
|
|
notify("选择器失效", msg.step + " 步骤的选择器未匹配到元素,请检查千帆页面是否改版。URL: " + msg.url);
|
|||
|
|
updateBadge("告警", "#f59e0b");
|
|||
|
|
sendResponse({ ok: true });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.type === "TEST_CONNECTION") {
|
|||
|
|
apiFetch("/api/health")
|
|||
|
|
.then((data) => sendResponse({ ok: true, data }))
|
|||
|
|
.catch((e) => sendResponse({ ok: false, error: e.message }));
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
if (msg.type === "FETCH_IMAGE") {
|
|||
|
|
(async () => {
|
|||
|
|
try {
|
|||
|
|
const config = await getConfig();
|
|||
|
|
const imageUrl = msg.url;
|
|||
|
|
let fetchUrl;
|
|||
|
|
if (imageUrl.startsWith("http")) {
|
|||
|
|
fetchUrl = imageUrl;
|
|||
|
|
} else if (imageUrl.startsWith("/")) {
|
|||
|
|
fetchUrl = config.serverUrl + imageUrl;
|
|||
|
|
} else {
|
|||
|
|
throw new Error("Invalid image URL: " + imageUrl);
|
|||
|
|
}
|
|||
|
|
const parsedUrl = new URL(fetchUrl);
|
|||
|
|
const serverUrl = new URL(config.serverUrl);
|
|||
|
|
const allowedServerImage = parsedUrl.origin === serverUrl.origin &&
|
|||
|
|
(parsedUrl.pathname.startsWith("/images/") || parsedUrl.pathname.startsWith("/api/images/"));
|
|||
|
|
if (!allowedServerImage && parsedUrl.protocol !== "https:") {
|
|||
|
|
throw new Error("只允许获取本地服务图片或 HTTPS 图片");
|
|||
|
|
}
|
|||
|
|
const res = await fetch(fetchUrl);
|
|||
|
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|||
|
|
const contentType = res.headers.get("Content-Type") || "";
|
|||
|
|
if (!contentType.startsWith("image/")) throw new Error("URL 返回的不是图片");
|
|||
|
|
const blob = await res.blob();
|
|||
|
|
const buffer = await blob.arrayBuffer();
|
|||
|
|
const bytes = new Uint8Array(buffer);
|
|||
|
|
let binary = "";
|
|||
|
|
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|||
|
|
const base64 = btoa(binary);
|
|||
|
|
const mimeType = blob.type || "image/jpeg";
|
|||
|
|
sendResponse({ ok: true, dataUrl: "data:" + mimeType + ";base64," + base64, mimeType });
|
|||
|
|
} catch (e) {
|
|||
|
|
sendResponse({ ok: false, error: e.message });
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Unknown message type
|
|||
|
|
sendResponse({ error: "unknown message type" });
|
|||
|
|
return false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Alarm-based backup polling (in case service worker restarts)
|
|||
|
|
chrome.alarms.create("poll", { periodInMinutes: 1 });
|
|||
|
|
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
|||
|
|
if (alarm.name === "poll") {
|
|||
|
|
const config = await getConfig();
|
|||
|
|
if (config.enabled && !state.polling) {
|
|||
|
|
startPolling();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Restore state on service worker startup
|
|||
|
|
loadState().then(async () => {
|
|||
|
|
const config = await getConfig();
|
|||
|
|
if (config.enabled) {
|
|||
|
|
startPolling();
|
|||
|
|
}
|
|||
|
|
});
|