From e48fcc53cdd2c6225497397e475175fe665cd936 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 10 Sep 2026 22:24:43 +0800 Subject: [PATCH] fix: make browser history sync reliable --- extension/chrome/background.js | 35 ++++++++++++++++++++---------- extension/chrome/options.html | 24 +++------------------ extension/chrome/options.js | 38 +++++++++++++++++++++++++++++++++ extension/firefox/background.js | 32 +++++++++++++++++++-------- extension/firefox/options.html | 25 +++------------------- extension/firefox/options.js | 38 +++++++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 63 deletions(-) create mode 100644 extension/chrome/options.js create mode 100644 extension/firefox/options.js diff --git a/extension/chrome/background.js b/extension/chrome/background.js index bcee467..c7422bb 100644 --- a/extension/chrome/background.js +++ b/extension/chrome/background.js @@ -71,13 +71,13 @@ function hostFrom(url) { } // ── 拉取最近浏览历史(增量)── -async function collectRecent() { - const since = Math.max(0, Date.now() - FETCH_WINDOW); +async function collectRecent(previousSyncTime = 0) { + const initialCutoff = Date.now() - DEDUP_SECONDS * 1000; + const cutoff = previousSyncTime > 0 ? previousSyncTime : initialCutoff; + const since = Math.max(0, Date.now() - FETCH_WINDOW, cutoff); const items = await new Promise((resolve) => { chrome.history.search({ text: '', startTime: since, maxResults: 10000 }, resolve); }); - - const cutoff = Date.now() - DEDUP_SECONDS * 1000; const seen = new Set(); const collected = []; @@ -88,7 +88,7 @@ async function collectRecent() { if (seen.has(key)) continue; seen.add(key); const ts = item.lastVisitTime || Date.now(); - if (ts < cutoff) continue; // 只收集最近窗口 + if (ts <= cutoff) continue; // 只上报上次成功同步之后的新访问 collected.push({ url: item.url, title: item.title || '', @@ -137,16 +137,27 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { async function syncNow() { ensureAlarm(); - const { serverUrl, syncToken, deviceName } = await new Promise((resolve) => { - chrome.storage.local.get(['serverUrl', 'syncToken', 'deviceName'], resolve); + const syncStartedAt = Date.now(); + const stored = await new Promise((resolve) => { + chrome.storage.local.get( + ['serverUrl', 'syncToken', 'deviceName', 'pending', 'lastSyncTime'], + resolve, + ); }); + const { serverUrl, syncToken, deviceName } = stored; if (!serverUrl) return { ok: false, reason: 'not configured' }; - // 增量拉取最近历史,合并到待上报队列 + // 每次从持久化存储恢复游标和失败队列,兼容 MV3 service worker 休眠重启。 + pendingItems = Array.isArray(stored.pending) ? stored.pending : []; + lastSyncTime = Number(stored.lastSyncTime) || 0; let recent; - try { recent = await collectRecent(); } catch (e) { recent = []; } + try { recent = await collectRecent(lastSyncTime); } catch (e) { recent = []; } const merged = dedup(recent.concat(pendingItems)); - if (merged.length === 0) return { ok: true, skipped: true, reason: 'nothing new' }; + if (merged.length === 0) { + lastSyncTime = syncStartedAt; + await new Promise((res) => chrome.storage.local.set({ lastSyncTime }, res)); + return { ok: true, skipped: true, reason: 'nothing new' }; + } const body = JSON.stringify({ items: merged.map((it) => ({ @@ -167,11 +178,13 @@ async function syncNow() { }); if (!resp.ok) { const text = await resp.text().catch(() => ''); + pendingItems = merged; + await new Promise((res) => chrome.storage.local.set({ pending: merged }, res)); return { ok: false, status: resp.status, error: text.slice(0, 200) }; } const data = await resp.json().catch(() => ({})); pendingItems = []; - lastSyncTime = Date.now(); + lastSyncTime = syncStartedAt; await new Promise((res) => chrome.storage.local.set({ pending: [], lastSyncTime }, res)); return { ok: true, count: merged.length, total: data.total || 0 }; } catch (e) { diff --git a/extension/chrome/options.html b/extension/chrome/options.html index 49355ae..de3d829 100644 --- a/extension/chrome/options.html +++ b/extension/chrome/options.html @@ -24,28 +24,10 @@ button{margin-top:14px;padding:8px 20px;border:none;border-radius:8px;font-weigh
- - + +
- + diff --git a/extension/chrome/options.js b/extension/chrome/options.js new file mode 100644 index 0000000..70102d5 --- /dev/null +++ b/extension/chrome/options.js @@ -0,0 +1,38 @@ +'use strict'; + +const api = chrome; +const urlInput = document.getElementById('url'); +const tokenInput = document.getElementById('token'); +const deviceInput = document.getElementById('device'); +const status = document.getElementById('status'); + +api.runtime.sendMessage({ action: 'getConfig' }, (config) => { + urlInput.value = config.serverUrl || ''; + tokenInput.value = config.syncToken || ''; + deviceInput.value = config.deviceName || ''; +}); + +document.getElementById('save').addEventListener('click', () => { + api.runtime.sendMessage({ + action: 'setConfig', + data: { + serverUrl: urlInput.value.replace(/\/$/, ''), + syncToken: tokenInput.value, + deviceName: deviceInput.value, + }, + }, () => { status.textContent = '已保存'; }); +}); + +document.getElementById('sync').addEventListener('click', () => { + api.runtime.sendMessage({ action: 'sync' }, (result) => { + if (!result) { + status.textContent = '失败: 扩展后台无响应'; + return; + } + status.textContent = result.skipped + ? '无待同步' + : result.ok + ? `同步 ${result.count} 条,共 ${result.total}` + : `失败: ${result.error || result.status}`; + }); +}); diff --git a/extension/firefox/background.js b/extension/firefox/background.js index 77d27f3..5232eda 100644 --- a/extension/firefox/background.js +++ b/extension/firefox/background.js @@ -61,12 +61,13 @@ function hostFrom(url) { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; } } -async function collectRecent() { - const since = Math.max(0, Date.now() - FETCH_WINDOW); +async function collectRecent(previousSyncTime = 0) { + const initialCutoff = Date.now() - DEDUP_SECONDS * 1000; + const cutoff = previousSyncTime > 0 ? previousSyncTime : initialCutoff; + const since = Math.max(0, Date.now() - FETCH_WINDOW, cutoff); const items = await new Promise((resolve) => { browser.history.search({ text: '', startTime: since, maxResults: 10000 }, resolve); }); - const cutoff = Date.now() - DEDUP_SECONDS * 1000; const seen = new Set(); const collected = []; for (const item of items) { @@ -75,7 +76,7 @@ async function collectRecent() { if (seen.has(key)) continue; seen.add(key); const ts = item.lastVisitTime || Date.now(); - if (ts < cutoff) continue; + if (ts <= cutoff) continue; collected.push({ url: item.url, title: item.title || '', @@ -113,15 +114,26 @@ browser.runtime.onMessage.addListener((msg, _sender, sendResponse) => { async function syncNow() { ensureAlarm(); - const { serverUrl, syncToken, deviceName } = await new Promise((resolve) => { - browser.storage.local.get(['serverUrl', 'syncToken', 'deviceName'], resolve); + const syncStartedAt = Date.now(); + const stored = await new Promise((resolve) => { + browser.storage.local.get( + ['serverUrl', 'syncToken', 'deviceName', 'pending', 'lastSyncTime'], + resolve, + ); }); + const { serverUrl, syncToken, deviceName } = stored; if (!serverUrl) return { ok: false, reason: 'not configured' }; + pendingItems = Array.isArray(stored.pending) ? stored.pending : []; + lastSyncTime = Number(stored.lastSyncTime) || 0; let recent; - try { recent = await collectRecent(); } catch (e) { recent = []; } + try { recent = await collectRecent(lastSyncTime); } catch (e) { recent = []; } const merged = dedup(recent.concat(pendingItems)); - if (merged.length === 0) return { ok: true, skipped: true, reason: 'nothing new' }; + if (merged.length === 0) { + lastSyncTime = syncStartedAt; + await new Promise((res) => browser.storage.local.set({ lastSyncTime }, res)); + return { ok: true, skipped: true, reason: 'nothing new' }; + } const body = JSON.stringify({ items: merged.map((it) => ({ @@ -142,11 +154,13 @@ async function syncNow() { }); if (!resp.ok) { const text = await resp.text().catch(() => ''); + pendingItems = merged; + await new Promise((res) => browser.storage.local.set({ pending: merged }, res)); return { ok: false, status: resp.status, error: text.slice(0, 200) }; } const data = await resp.json().catch(() => ({})); pendingItems = []; - lastSyncTime = Date.now(); + lastSyncTime = syncStartedAt; await new Promise((res) => browser.storage.local.set({ pending: [], lastSyncTime }, res)); return { ok: true, count: merged.length, total: data.total || 0 }; } catch (e) { diff --git a/extension/firefox/options.html b/extension/firefox/options.html index cefca0d..de3d829 100644 --- a/extension/firefox/options.html +++ b/extension/firefox/options.html @@ -14,7 +14,6 @@ button{margin-top:14px;padding:8px 20px;border:none;border-radius:8px;font-weigh .sync{background:#22c55e;color:#fff;margin-left:8px} .status{margin-top:12px;font-size:12px;color:#64748b} -

JianF Nav Sync

@@ -25,28 +24,10 @@ button{margin-top:14px;padding:8px 20px;border:none;border-radius:8px;font-weigh
- - + +
- + diff --git a/extension/firefox/options.js b/extension/firefox/options.js new file mode 100644 index 0000000..de900ee --- /dev/null +++ b/extension/firefox/options.js @@ -0,0 +1,38 @@ +'use strict'; + +const api = typeof browser !== 'undefined' ? browser : chrome; +const urlInput = document.getElementById('url'); +const tokenInput = document.getElementById('token'); +const deviceInput = document.getElementById('device'); +const status = document.getElementById('status'); + +api.runtime.sendMessage({ action: 'getConfig' }, (config) => { + urlInput.value = config.serverUrl || ''; + tokenInput.value = config.syncToken || ''; + deviceInput.value = config.deviceName || ''; +}); + +document.getElementById('save').addEventListener('click', () => { + api.runtime.sendMessage({ + action: 'setConfig', + data: { + serverUrl: urlInput.value.replace(/\/$/, ''), + syncToken: tokenInput.value, + deviceName: deviceInput.value, + }, + }, () => { status.textContent = '已保存'; }); +}); + +document.getElementById('sync').addEventListener('click', () => { + api.runtime.sendMessage({ action: 'sync' }, (result) => { + if (!result) { + status.textContent = '失败: 扩展后台无响应'; + return; + } + status.textContent = result.skipped + ? '无待同步' + : result.ok + ? `同步 ${result.count} 条,共 ${result.total}` + : `失败: ${result.error || result.status}`; + }); +});