fix: исправление 6 пунктов из ревью

Critical:
- bridge: AutoGenPort=false по умолчанию, не генерируем secret если пустой
  → extension и bridge совпадают на port 9786 и empty secret
- bridgeConfig: убрана авто-генерация secret, убран secret из BridgeInfo

High:
- extension/background.js + extension-firefox/background.js:
  все chrome.* listeners вынесены в global scope (не внутри onInstalled/onStartup)
  → MV3 service worker корректно перезапускается
- UI: acceptBrowserEvent вызывает AcceptBrowserEvent, attachBrowserEvent вызывает
  AttachBrowserEventToNode (к текущему selectedNode), а не DismissBrowserEvent
- watcher: при Create проверяется isUnderVault(absPath, vaultRoot) —
  если файл уже в vault, используется AddExternal вместо CopyIntoVault
  → нет дублирования файлов с timestamp-суффиксом

Medium:
- bridge.Event: добавлено поле DeviceID, handleEvents обогащает events из batch.DeviceID
  → device_id сохраняется в DB как chrome-*/firefox-*, а не evt_*
- config: FileWatcher изменён на *bool — nil означает default true,
  false = явно выключено → старые config.json без поля file_watcher получают true
This commit is contained in:
2026-06-07 00:15:34 +08:00
parent b676ac675a
commit 1cc0c407b1
9 changed files with 142 additions and 137 deletions
+49 -58
View File
@@ -1,12 +1,17 @@
// Verstak Bridge — Background Service Worker
// Verstak Bridge — Background Service Worker (Chrome)
// Tracks active tab changes and queues browser events.
// Pushes events to the Verstak HTTP bridge when available.
//
// IMPORTANT: All chrome.* event listeners are registered at global scope,
// NOT inside onInstalled/onStartup. MV3 service workers can be restarted at
// any time; listeners inside lifecycle hooks are lost on restart.
// See: https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/events
const STORAGE_KEY = 'verstak_queue';
const BRIDGE_KEY = 'verstak_bridge_config';
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: true };
const FLUSH_INTERVAL_MS = 30_000; // flush every 30s
const IDLE_RESET_MS = 60_000; // reset session after 60s idle
const RECENT_KEY = 'verstak_recent';
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: false };
const IDLE_RESET_MS = 60_000;
// Session state
let session = {
@@ -17,43 +22,55 @@ let session = {
startedAt: null,
};
// Start tracking on install
// --- Global event listeners (registered once at script load) ---
chrome.runtime.onInstalled.addListener(() => {
setupAlarms();
setupListeners();
});
// Wake up on browser start
chrome.runtime.onStartup.addListener(() => {
setupAlarms();
setupListeners();
});
// Tab tracking — global scope
chrome.tabs.onActivated.addListener(onTabActivated);
chrome.tabs.onUpdated.addListener(onTabUpdated);
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
// Alarm handler — global scope
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'flushEvents') flushSession();
if (alarm.name === 'pingBridge') pingBridge();
});
// Message handler — global scope
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FORCE_FLUSH') {
flushSession();
flushQueue();
sendResponse({ ok: true });
}
if (msg.type === 'SET_TRACKING') {
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
sendResponse({ ok: true });
}
return true;
});
// --- Alarm setup ---
function setupAlarms() {
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
}
function setupListeners() {
// Track active tab changes
chrome.tabs.onActivated.addListener(onTabActivated);
chrome.tabs.onUpdated.addListener(onTabUpdated);
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
// --- Tab tracking ---
// Listen for flush alarm
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'flushEvents') flushSession();
if (alarm.name === 'pingBridge') pingBridge();
});
}
// Called when user switches tabs
function onTabActivated(activeInfo) {
flushSession();
startTracking(activeInfo.tabId);
}
// Called when a tab's URL or title changes
function onTabUpdated(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete' && tab.active) {
flushSession();
@@ -61,14 +78,11 @@ function onTabUpdated(tabId, changeInfo, tab) {
}
}
// Called when window focus changes
function onWindowFocusChanged(windowId) {
if (windowId === chrome.windows.WINDOW_ID_NONE) {
// Window lost focus — flush and stop session
flushSession();
session = { url: '', title: '', domain: '', tabId: -1, startedAt: null };
} else {
// Window gained focus — get active tab and start tracking
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs && tabs.length > 0) {
startTracking(tabs[0].id);
@@ -86,19 +100,20 @@ function startTracking(tabId) {
url: tab.url,
title: tab.title || '',
domain: extractDomain(tab.url),
tabId: tabId,
tabId,
startedAt: Date.now(),
};
});
}
// --- Session flush ---
function flushSession() {
if (!session.url || !session.startedAt) return;
const duration = Math.round((Date.now() - session.startedAt) / 1000);
if (duration < 2) {
// Skip very short visits
session.startedAt = Date.now(); // reset timer
session.startedAt = Date.now();
return;
}
@@ -114,7 +129,7 @@ function flushSession() {
};
queueEvent(event);
session.startedAt = Date.now(); // reset for next segment
session.startedAt = Date.now();
}
function queueEvent(event) {
@@ -122,13 +137,11 @@ function queueEvent(event) {
const queue = data[STORAGE_KEY] || [];
queue.push(event);
// Keep max 500 events in queue
if (queue.length > 500) {
queue.splice(0, queue.length - 500);
}
chrome.storage.local.set({ [STORAGE_KEY]: queue }, () => {
// Immediately try to flush if we have enough events
if (queue.length >= 5) {
flushQueue();
}
@@ -136,6 +149,8 @@ function queueEvent(event) {
});
}
// --- Bridge communication ---
function flushQueue() {
chrome.storage.local.get([STORAGE_KEY, BRIDGE_KEY], (data) => {
const queue = data[STORAGE_KEY] || [];
@@ -161,23 +176,17 @@ function flushQueue() {
})
.then((res) => {
if (res.ok) {
// Update recent events
for (const ev of queue) {
updateRecent(ev);
}
// Clear queue on success
chrome.storage.local.set({ [STORAGE_KEY]: [] });
// Notify popup if open
chrome.runtime.sendMessage({ type: 'UI_UPDATE' }).catch(() => {});
} else if (res.status === 401) {
console.warn('[verstak] bridge auth failed, check secret');
}
})
.catch((err) => {
.catch(() => {
// Bridge not available — keep events in queue
if (queue.length % 10 === 0) {
console.debug('[verstak] bridge unavailable:', err.message);
}
});
});
}
@@ -202,23 +211,8 @@ function pingBridge() {
});
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FORCE_FLUSH') {
flushSession();
flushQueue();
sendResponse({ ok: true });
}
if (msg.type === 'SET_TRACKING') {
// Tracking is always on at the background level;
// the popup just shows the setting.
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
sendResponse({ ok: true });
}
});
// --- Recent events ---
// Update recent events from flushed queue entries.
// This is called by flushQueue on successful delivery.
function updateRecent(event) {
chrome.storage.local.get(RECENT_KEY, (data) => {
const recent = data[RECENT_KEY] || [];
@@ -229,7 +223,6 @@ function updateRecent(event) {
active_seconds: event.active_seconds,
ts: new Date().toISOString(),
});
// Keep last 50
if (recent.length > 50) {
recent.splice(0, recent.length - 50);
}
@@ -237,9 +230,7 @@ function updateRecent(event) {
});
}
const RECENT_KEY = 'verstak_recent';
// ... existing code continues below ...
// --- Helpers ---
function extractDomain(url) {
try {