This commit is contained in:
Rocky Kwok committed 2026-09-04 11:16:38 +08:00
1 parent 49d9ced04f
commit a723a9ca02
3 files changed
+241 -19

No files matched your search

@@ -5,6 +5,9 @@ const LEGACY_LOCAL_API_BASE_URL = "http://localhost:8001";
const LEGACY_LOCAL_WEB_BASE_URL = "http://localhost:5174";
let pollTimer = null;
let isPolling = false;
let startupPromise = null;
const POLLING_SCHEDULE_SETTING_KEYS = new Set(["taskPollingEnabled", "taskPollIntervalSeconds"]);
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -12,6 +15,14 @@ function delay(ms) {
async function getSettings() {
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
const discovered = await chrome.storage.local.get({
discoveredApiBaseUrl: "",
discoveredWebBaseUrl: "",
});
if (settings.autoDiscoverServiceUrl) {
settings.apiBaseUrl = discovered.discoveredApiBaseUrl || settings.apiBaseUrl;
settings.webBaseUrl = discovered.discoveredWebBaseUrl || settings.webBaseUrl;
}
const updates = {};
if (!settings.apiBaseUrl || settings.apiBaseUrl === LEGACY_LOCAL_API_BASE_URL) {
updates.apiBaseUrl = DEER_CLIPPER_DEFAULTS.apiBaseUrl;
@@ -69,10 +80,7 @@ function apiCandidatesForWebBase(webBase, currentApiBase) {
}
};
add(currentApiBase);
add(webBase);
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
add(LEGACY_LOCAL_API_BASE_URL);
try {
const url = new URL(normalizeServiceBase(webBase));
@@ -85,6 +93,10 @@ function apiCandidatesForWebBase(webBase, currentApiBase) {
// Ignore invalid candidates.
}
add(currentApiBase);
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
add(LEGACY_LOCAL_API_BASE_URL);
return candidates;
}
@@ -93,6 +105,12 @@ async function browserContextStatus(apiBaseUrl) {
const response = await fetch(`${apiBaseUrl}/api/browser-context/status`, {
credentials: "include",
});
// The gateway may enforce authentication globally before this otherwise
// public discovery endpoint is reached. A 401/403 still proves that this
// candidate is the zncm API paired with the open frontend tab.
if (response.status === 401 || response.status === 403) {
return { enabled: true, authenticationRequired: true };
}
if (!response.ok) {
return null;
}
@@ -201,9 +219,12 @@ async function discoverServiceBases(settings, force = false) {
settings.webBaseUrl !== next.webBaseUrl ||
settings.lastServiceSyncSource !== next.lastServiceSyncSource
) {
await chrome.storage.sync.set({
apiBaseUrl: next.apiBaseUrl,
webBaseUrl: next.webBaseUrl,
// Discovered endpoints are runtime state, not user-authored preferences.
// Keep them in storage.local so Chrome sync write quotas cannot break task
// polling. The options page may still persist explicit user choices.
await chrome.storage.local.set({
discoveredApiBaseUrl: next.apiBaseUrl,
discoveredWebBaseUrl: next.webBaseUrl,
lastServiceSyncAt: next.lastServiceSyncAt,
lastServiceSyncSource: next.lastServiceSyncSource,
});
@@ -826,19 +847,50 @@ async function pollOnce() {
}
}
async function startup() {
async function initializePolling() {
let settings = await getSettings();
settings = await discoverServiceBases(settings);
settings = await syncAuthToken(settings);
try {
settings = await discoverServiceBases(settings);
} catch (error) {
// Service discovery persists diagnostic metadata in storage.sync. Chrome can
// temporarily reject those writes after its quota is reached; polling must
// still start with the last known settings.
console.warn("zncm service discovery failed during startup", error);
}
try {
settings = await syncAuthToken(settings);
} catch (error) {
// A failed status/token write must not prevent alarms from being installed.
// pollOnce() will retry token synchronization before claiming each task.
console.warn("zncm token sync failed during startup", error);
}
await refreshAlarm(settings);
await scheduleNextPoll(1000);
}
chrome.runtime.onInstalled.addListener(() => void startup());
chrome.runtime.onStartup.addListener(() => void startup());
function startup() {
if (!startupPromise) {
startupPromise = initializePolling().finally(() => {
startupPromise = null;
});
}
return startupPromise;
}
async function restartPollingSchedule() {
const settings = await getSettings();
await refreshAlarm(settings);
await scheduleNextPoll(0);
}
chrome.runtime.onInstalled.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
chrome.runtime.onStartup.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === "sync") {
void startup();
if (
areaName === "sync" &&
Object.keys(changes || {}).some((key) => POLLING_SCHEDULE_SETTING_KEYS.has(key))
) {
void restartPollingSchedule().catch((error) => console.warn("zncm polling reschedule failed", error));
}
});
chrome.alarms.onAlarm.addListener((alarm) => {
@@ -902,4 +954,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
});
void startup();
void startup().catch((error) => console.warn("zncm startup failed", error));