fix: make browser history sync reliable
This commit is contained in:
@ -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) {
|
||||
|
||||
@ -24,28 +24,10 @@ button{margin-top:14px;padding:8px 20px;border:none;border-radius:8px;font-weigh
|
||||
<label>设备名称</label>
|
||||
<input id="device" placeholder="desktop-chrome">
|
||||
<div>
|
||||
<button class="save" onclick="save()">保存</button>
|
||||
<button class="sync" onclick="doSync()">立即同步</button>
|
||||
<button class="save" id="save">保存</button>
|
||||
<button class="sync" id="sync">立即同步</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
<script>
|
||||
chrome.runtime.sendMessage({action:'getConfig'},(d)=>{
|
||||
document.getElementById('url').value=d.serverUrl||'';
|
||||
document.getElementById('token').value=d.syncToken||'';
|
||||
document.getElementById('device').value=d.deviceName||'';
|
||||
});
|
||||
function save(){
|
||||
chrome.runtime.sendMessage({action:'setConfig',data:{
|
||||
serverUrl:document.getElementById('url').value.replace(/\/$/,''),
|
||||
syncToken:document.getElementById('token').value,
|
||||
deviceName:document.getElementById('device').value,
|
||||
}},()=>{document.getElementById('status').textContent='已保存 OK'});
|
||||
}
|
||||
function doSync(){
|
||||
chrome.runtime.sendMessage({action:'sync'},(r)=>{
|
||||
document.getElementById('status').textContent=r.skipped?'无待同步':r.ok?`同步 ${r.count} 条,共 ${r.total}`:`失败: ${r.error||r.status}`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
38
extension/chrome/options.js
Normal file
38
extension/chrome/options.js
Normal file
@ -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}`;
|
||||
});
|
||||
});
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
</style>
|
||||
<script>if (typeof browser !== "undefined" && typeof chrome === "undefined") { window.chrome = browser; } else if (typeof browser !== "undefined") { window.chrome = window.chrome || browser; }</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1> JianF Nav Sync</h1>
|
||||
@ -25,28 +24,10 @@ button{margin-top:14px;padding:8px 20px;border:none;border-radius:8px;font-weigh
|
||||
<label>设备名称</label>
|
||||
<input id="device" placeholder="desktop-chrome">
|
||||
<div>
|
||||
<button class="save" onclick="save()">保存</button>
|
||||
<button class="sync" onclick="doSync()">立即同步</button>
|
||||
<button class="save" id="save">保存</button>
|
||||
<button class="sync" id="sync">立即同步</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
<script>
|
||||
chrome.runtime.sendMessage({action:'getConfig'},(d)=>{
|
||||
document.getElementById('url').value=d.serverUrl||'';
|
||||
document.getElementById('token').value=d.syncToken||'';
|
||||
document.getElementById('device').value=d.deviceName||'';
|
||||
});
|
||||
function save(){
|
||||
chrome.runtime.sendMessage({action:'setConfig',data:{
|
||||
serverUrl:document.getElementById('url').value.replace(/\/$/,''),
|
||||
syncToken:document.getElementById('token').value,
|
||||
deviceName:document.getElementById('device').value,
|
||||
}},()=>{document.getElementById('status').textContent='已保存 OK'});
|
||||
}
|
||||
function doSync(){
|
||||
chrome.runtime.sendMessage({action:'sync'},(r)=>{
|
||||
document.getElementById('status').textContent=r.skipped?'无待同步':r.ok?`同步 ${r.count} 条,共 ${r.total}`:`失败: ${r.error||r.status}`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
38
extension/firefox/options.js
Normal file
38
extension/firefox/options.js
Normal file
@ -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}`;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user