-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
269 lines (223 loc) · 8.37 KB
/
background.js
File metadata and controls
269 lines (223 loc) · 8.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// load shared modules
importScripts('api-utils.js');
importScripts('i18n.js');
// Enable the side panel to open when the action icon is clicked
chrome.sidePanel
.setPanelBehavior({ openPanelOnActionClick: true })
.catch((error) => console.error('Failed to set panel behavior:', error));
// track last-seen URL per tab to avoid re-processing on same-URL updates
const tabUrls = new Map();
// listen for tab updates to trigger auto summary
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
// only process when page load is complete
if (changeInfo.status !== 'complete') return;
// skip special pages
if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) {
return;
}
// check if URL has changed for this tab
const lastUrl = tabUrls.get(tabId);
if (lastUrl === tab.url) {
return; // same URL, don't re-process
}
tabUrls.set(tabId, tab.url);
// check if auto sum is enabled
const settings = await chrome.storage.sync.get({ autoSumEnabled: false });
if (!settings.autoSumEnabled) return;
// trigger auto summary
await triggerAutoSum(tabId, tab.url);
});
// clean up when tab is closed
chrome.tabs.onRemoved.addListener((tabId) => {
tabUrls.delete(tabId);
});
// send message to tab with retry (content script may not be ready immediately)
// total timeout of 5s prevents indefinite blocking if tab is unresponsive
async function sendMessageWithRetry(tabId, message, maxRetries) {
if (!maxRetries) maxRetries = 3;
var lastError = null;
var deadline = Date.now() + 5000; // 5s total timeout
for (var i = 0; i < maxRetries; i++) {
if (Date.now() >= deadline) break;
try {
return await chrome.tabs.sendMessage(tabId, message);
} catch (err) {
lastError = err;
// wait before retry: 300ms, 600ms, 900ms
var delay = Math.min((i + 1) * 300, deadline - Date.now());
if (delay <= 0) break;
await new Promise(function(resolve) {
setTimeout(resolve, delay);
});
}
}
throw lastError;
}
// trigger auto summary for a tab
async function triggerAutoSum(tabId, url) {
// skip auto summary if cloud consent not yet given
var provider = (await chrome.storage.sync.get({ provider: API_DEFAULTS.provider })).provider;
var preset = API_PROVIDERS[provider];
if (preset && preset.requiresKey) {
var consent = await chrome.storage.local.get({ cloudConsentGiven: false });
if (!consent.cloudConsentGiven) return;
}
try {
await chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['autosum.js']
});
await new Promise(resolve => setTimeout(resolve, 400));
var langSettings = await chrome.storage.sync.get({ lang: I18N_DEFAULT });
var lang = langSettings.lang || I18N_DEFAULT;
await sendMessageWithRetry(tabId, { action: 'showAutoSum', lang: lang }, 3);
// directly execute content extraction function in page context
const results = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: extractPageContent
});
const pageContent = results?.[0]?.result || '';
// removed verbose logging
if (!pageContent || pageContent.length < 50) {
await chrome.tabs.sendMessage(tabId, {
action: 'autoSumResult',
success: false,
error: 'No content to summarize (length: ' + pageContent.length + ')'
});
return;
}
// truncate content if too long (for auto summary, use smaller context)
const maxLength = 8000;
let truncatedContent = pageContent;
if (truncatedContent.length > maxLength) {
truncatedContent = truncatedContent.substring(0, maxLength) + '\n\n[Content truncated...]';
}
// call API to summarize
const summary = await callSummarizeAPI(truncatedContent, url);
// send result to content script
await chrome.tabs.sendMessage(tabId, {
action: 'autoSumResult',
success: true,
summary: summary
});
} catch (error) {
console.error('Auto summary error:', error);
try {
await chrome.tabs.sendMessage(tabId, {
action: 'autoSumResult',
success: false,
error: error.message || 'Failed to summarize'
});
} catch (e) {
// tab might be closed
}
}
}
// content extraction function - runs in page context
function extractPageContent() {
// try user selection first
const selection = window.getSelection().toString().trim();
if (selection && selection.length > 0) {
return selection;
}
// clone body to avoid modifying original DOM
const clone = document.body.cloneNode(true);
// remove junk elements
const junkSelectors = [
'script', 'style', 'noscript', 'iframe', 'svg',
'.ad', '.ads', '.advertisement'
];
junkSelectors.forEach(sel => {
clone.querySelectorAll(sel).forEach(el => el.remove());
});
// gather meta info
let metaInfo = "";
const title = document.title;
const description = document.querySelector('meta[name="description"]')?.content;
const keywords = document.querySelector('meta[name="keywords"]')?.content;
const pageUrl = window.location.href;
metaInfo += `Title: ${title}\n`;
metaInfo += `URL: ${pageUrl}\n`;
if (description) metaInfo += `Description: ${description}\n`;
if (keywords) metaInfo += `Keywords: ${keywords}\n`;
metaInfo += `----------------------------------------\n\n`;
// get visible text
let visibleText = clone.innerText || clone.textContent || '';
// clean up whitespace
visibleText = visibleText
.replace(/\n\s*\n\s*\n/g, '\n\n')
.replace(/[ \t]+/g, ' ');
const lines = visibleText.split('\n');
const cleanedLines = lines
.map(line => line.trim())
.filter(line => line.length > 0);
visibleText = cleanedLines.join('\n');
// fallback if too short
if (!visibleText || visibleText.length < 50) {
return metaInfo + (document.body.innerText || document.body.textContent || '');
}
return metaInfo + visibleText;
}
// call API to get summary (uses shared api-utils.js functions)
async function callSummarizeAPI(content, url) {
var settings = await chrome.storage.sync.get({
lang: I18N_DEFAULT,
provider: API_DEFAULTS.provider,
apiFormat: API_DEFAULTS.apiFormat,
baseUrl: API_DEFAULTS.baseUrl,
model: API_DEFAULTS.model,
reasoningEffort: API_DEFAULTS.reasoningEffort
});
// load API key from local storage (never synced to Google account)
var localData = await chrome.storage.local.get({ apiKey: API_DEFAULTS.apiKey });
settings.apiKey = localData.apiKey;
if (!settings.baseUrl) {
throw new Error('API not configured. Please check settings.');
}
// ollama doesn't require a key; cloud providers do
var preset = API_PROVIDERS[settings.provider];
if (preset && preset.requiresKey && !settings.apiKey) {
throw new Error('API key not configured. Please check settings.');
}
var lang = settings.lang || I18N_DEFAULT;
var promptPrefix = getAutoSumPromptForLang(lang);
var userPrompt = promptPrefix
+ 'URL: ' + url + '\n\n'
+ 'Content:\n' + content;
var messages = [{ role: 'user', content: userPrompt }];
var apiUrl = constructApiUrl(settings);
var headers = buildApiHeaders(settings);
var payload = buildApiPayload(messages, settings);
var response = await fetch(apiUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
var responseText = await response.text();
if (!response.ok) {
throw new Error('API Error ' + response.status + ': ' + responseText.substring(0, 200));
}
var data;
try {
data = JSON.parse(responseText);
} catch (e) {
throw new Error('Failed to parse API response as JSON');
}
var summary = extractApiResponse(data);
if (!summary || summary.trim() === '') {
console.error('Could not extract summary from response structure:', Object.keys(data));
throw new Error('API returned empty response. Check console for details.');
}
return summary.trim();
}
// listen for retry requests from content script.
// PERF: only return true (keep port open) when we actually handle the message.
// returning true for unhandled messages leaks the message port.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'retryAutoSum' && sender.tab) {
triggerAutoSum(sender.tab.id, sender.tab.url);
sendResponse({ status: 'retrying' });
return true;
}
// unhandled message: return false (default) to close the port immediately
});