-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
422 lines (369 loc) · 13.8 KB
/
Copy pathbackground.js
File metadata and controls
422 lines (369 loc) · 13.8 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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
// Background service worker for 1SO SearchHub
const TAB_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan', 'orange'];
let colorIndex = 0;
let tabGroups = new Map(); // Track tab groups by search query
// Initialize on install
chrome.runtime.onInstalled.addListener(async () => {
console.log('1SO SearchHub installed');
// Set up side panel behavior
try {
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
console.log('Side panel behavior configured');
} catch (error) {
console.error('Failed to configure side panel:', error);
}
setupContextMenus();
cleanupOldGroups();
});
// Extension icon click handler - open main page
chrome.action.onClicked.addListener(() => {
chrome.tabs.create({ url: 'index.html' });
});
// Message handler
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'search') {
handleSearch(message);
return true;
} else if (message.action === 'followUp') {
// 中继追问到所有 AI 页面标签(content.js 在另一端接收)
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
if (tab.id !== sender.tab?.id) {
chrome.tabs.sendMessage(tab.id, { action: 'followUp', question: message.question }).catch(() => {});
}
});
});
return true;
} else if (message.action === 'getSettings') {
chrome.storage.sync.get(['settings'], (result) => {
sendResponse({ settings: result.settings });
});
return true;
}
});
// Handle search action
async function handleSearch(message) {
const { query, urls, settings } = message;
try {
// Save last query
await chrome.storage.local.set({ lastQuery: query });
if (settings.openMode === 'window') {
// Open in multiple windows
await openInWindows(urls, query, settings);
} else {
// Open in tabs (default)
await openInTabs(urls, query, settings);
}
} catch (error) {
console.error('Search failed:', error);
}
}
// Open search results in tabs
async function openInTabs(urls, query, settings) {
try {
// Get current window ID first
const [currentTab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
const windowId = currentTab ? currentTab.windowId : null;
// Create all tabs
const createdTabs = [];
for (const item of urls) {
const tab = await chrome.tabs.create({ url: item.url, active: false });
createdTabs.push(tab);
}
// Group tabs if enabled
if (settings.groupTabs && createdTabs.length > 0) {
const tabIds = createdTabs.map(tab => tab.id);
const groupId = await chrome.tabs.group({ tabIds });
// Update group properties
await chrome.tabGroups.update(groupId, {
title: query.length > 20 ? query.substring(0, 20) + '...' : query,
color: TAB_COLORS[colorIndex % TAB_COLORS.length],
collapsed: settings.collapsedGroups
});
colorIndex++;
// Store group info
tabGroups.set(groupId, {
query,
tabIds,
timestamp: Date.now()
});
}
// Focus first tab
if (createdTabs.length > 0) {
await chrome.tabs.update(createdTabs[0].id, { active: true });
}
// Always show sidebar after search (for vertical tabs management)
// Delay slightly to ensure tabs are created
if (windowId) {
setTimeout(async () => {
try {
await chrome.sidePanel.open({ windowId: windowId });
console.log('Sidebar opened successfully for window:', windowId);
} catch (error) {
console.error('Failed to open sidebar:', error);
// Try alternative method
try {
await chrome.sidePanel.setOptions({
path: 'sidebar.html',
enabled: true
});
await chrome.sidePanel.open({ windowId: windowId });
console.log('Sidebar opened with alternative method');
} catch (err) {
console.error('Alternative method also failed:', err);
}
}
}, 100);
}
} catch (error) {
console.error('Failed to open tabs:', error);
}
}
// Open search results in windows
async function openInWindows(urls, query, settings) {
try {
// Get display info
const displays = await chrome.system.display.getInfo();
if (displays.length === 0) {
console.error('No display found');
return;
}
const display = displays[0];
const workArea = display.workArea;
const windowWidth = Math.floor(workArea.width / urls.length);
const windowHeight = workArea.height;
// Create windows side by side
for (let i = 0; i < urls.length; i++) {
const left = workArea.left + (i * windowWidth);
const top = workArea.top;
await chrome.windows.create({
url: urls[i].url,
left,
top,
width: windowWidth,
height: windowHeight,
type: 'normal',
focused: i === 0
});
}
} catch (error) {
console.error('Failed to open windows:', error);
}
}
// Note: setupContextMenus and cleanupOldGroups are called in the main onInstalled listener above
function setupContextMenus() {
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: 'searchHub-search',
title: 'Search with 1SO SearchHub',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'searchHub-separator',
type: 'separator',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'searchHub-settings',
title: '1SO SearchHub Settings',
contexts: ['action']
});
});
}
// Context menu click handler
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'searchHub-search') {
handleContextMenuSearch(info.selectionText, tab);
} else if (info.menuItemId === 'searchHub-settings') {
chrome.tabs.create({ url: 'settings.html' });
}
});
async function handleContextMenuSearch(text, tab) {
try {
// Get selected engines
const result = await chrome.storage.local.get(['selectedEngines']);
const selectedEngines = result.selectedEngines || [];
if (selectedEngines.length === 0) {
// Show notification if no engines selected
await showNotification('Please select search engines first', 'Open 1SO SearchHub to select engines');
return;
}
// Get settings
const settingsResult = await chrome.storage.sync.get(['settings']);
const settings = settingsResult.settings || {};
// Build URLs
const urls = [];
for (const engineId of selectedEngines) {
const engine = await findEngineById(engineId);
if (engine) {
urls.push({
url: engine.url.replace('%s', encodeURIComponent(text)),
name: engine.name
});
}
}
// Perform search
await handleSearch({
query: text,
urls,
settings
});
} catch (error) {
console.error('Context menu search failed:', error);
}
}
// Find engine by ID (including custom engines)
async function findEngineById(engineId) {
// First check default engines
const defaultEngine = findDefaultEngineById(engineId);
if (defaultEngine) {
return defaultEngine;
}
// Then check custom engines
try {
const result = await chrome.storage.sync.get(['settings']);
const customEngines = result.settings?.customEngines || [];
const customEngine = customEngines.find(e => e.id === engineId);
if (customEngine) {
return {
url: customEngine.url,
name: customEngine.name
};
}
} catch (error) {
console.error('Failed to load custom engines:', error);
}
return null;
}
// Find engine by ID from default engines only
function findDefaultEngineById(engineId) {
// 与 scripts/config.js 保持同步(v1.3.0)
const engineMap = {
// AI Search
'perplexity': { url: 'https://www.perplexity.ai/search?q=%s', name: 'Perplexity' },
'metaso': { url: 'https://metaso.cn/?q=%s', name: '秘塔 Metaso' },
'genspark': { url: 'https://www.genspark.ai/search?q=%s', name: 'Genspark' },
'felo': { url: 'https://felo.ai/search?q=%s', name: 'Felo' },
'you': { url: 'https://you.com/search?q=%s', name: 'You.com' },
'exa': { url: 'https://exa.ai/search?q=%s', name: 'Exa' },
'monica': { url: 'https://s.monica.im/search?q=%s', name: 'Monica' },
// AI Chat
'doubao': { url: 'https://www.doubao.com/chat/?q=%s', name: '豆包 Doubao' },
'deepseek': { url: 'https://chat.deepseek.com/?q=%s', name: 'DeepSeek' },
'kimi': { url: 'https://www.kimi.com/?q=%s', name: 'Kimi' },
'hunyuan': { url: 'https://yuanbao.tencent.com/?q=%s', name: '腾讯元宝' },
'wenxin': { url: 'https://wenxin.baidu.com/?q=%s', name: '文心一言' },
'tongyi': { url: 'https://www.qianwen.com/?q=%s', name: '通义千问' },
'zhipu': { url: 'https://chatglm.cn/?q=%s', name: '智谱清言' },
'minimax': { url: 'https://hailuoai.com/?q=%s', name: '海螺 MiniMax' },
'mimo': { url: 'https://aistudio.xiaomimimo.com/?q=%s', name: '小米 MiMo' },
'xinghuo': { url: 'https://xinghuo.xfyun.cn/desk?q=%s', name: '讯飞星火' },
'tiangong': { url: 'https://www.tiangong.cn/?q=%s', name: '昆仑天工' },
'yuewen': { url: 'https://chat.stepfun.com/?q=%s', name: '阶跃跃问' },
'chatgpt': { url: 'https://chatgpt.com/?q=%s', name: 'ChatGPT' },
'claude': { url: 'https://claude.ai/new?q=%s', name: 'Claude' },
'gemini': { url: 'https://gemini.google.com/app?q=%s', name: 'Gemini' },
'grok': { url: 'https://grok.com/?q=%s', name: 'Grok' },
// Traditional
'baidu': { url: 'https://www.baidu.com/s?wd=%s', name: '百度' },
'google': { url: 'https://www.google.com/search?q=%s', name: 'Google' },
'bing': { url: 'https://www.bing.com/search?q=%s', name: 'Bing' },
'sogou': { url: 'https://www.sogou.com/web?query=%s', name: '搜狗' },
'duckduckgo': { url: 'https://duckduckgo.com/?q=%s', name: 'DuckDuckGo' },
// Social
'xiaohongshu': { url: 'https://www.xiaohongshu.com/search_result?keyword=%s', name: '小红书' },
'douyin': { url: 'https://www.douyin.com/search/%s', name: '抖音' },
'weibo': { url: 'https://s.weibo.com/weibo?q=%s', name: '微博' },
'zhihu': { url: 'https://www.zhihu.com/search?q=%s', name: '知乎' },
'twitter': { url: 'https://twitter.com/search?q=%s', name: 'Twitter/X' },
'reddit': { url: 'https://www.reddit.com/search/?q=%s', name: 'Reddit' },
'facebook': { url: 'https://www.facebook.com/search/top/?q=%s', name: 'Facebook' },
// Video
'bilibili': { url: 'https://search.bilibili.com/all?keyword=%s', name: '哔哩哔哩' },
'xigua': { url: 'https://www.ixigua.com/search/%s', name: '西瓜视频' },
'youtube': { url: 'https://www.youtube.com/results?search_query=%s', name: 'YouTube' },
// Knowledge
'baike': { url: 'https://baike.baidu.com/search?word=%s', name: '百度百科' },
'zhihu_k': { url: 'https://www.zhihu.com/search?q=%s', name: '知乎' },
'wikipedia': { url: 'https://zh.wikipedia.org/wiki/Special:Search?search=%s', name: '维基百科' },
// Maps
'amap': { url: 'https://www.amap.com/search?query=%s', name: '高德地图' },
'baidumap': { url: 'https://map.baidu.com/search?wd=%s', name: '百度地图' },
'qqmap': { url: 'https://map.qq.com/?what=%s', name: '腾讯地图' },
// Developer
'github': { url: 'https://github.com/search?q=%s', name: 'GitHub' },
'stackoverflow': { url: 'https://stackoverflow.com/search?q=%s', name: 'Stack Overflow' },
// Shopping
'taobao': { url: 'https://s.taobao.com/search?q=%s', name: '淘宝' },
'jd': { url: 'https://search.jd.com/Search?keyword=%s', name: '京东' },
'pinduoduo': { url: 'https://mobile.yangkeduo.com/search_result.html?search_key=%s', name: '拼多多' },
'amazon': { url: 'https://www.amazon.com/s?k=%s', name: 'Amazon' }
};
return engineMap[engineId];
}
// Keyboard shortcut handler
chrome.commands.onCommand.addListener((command) => {
if (command === 'search_selected') {
// Get selected text from active tab
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
if (tabs[0]) {
try {
const results = await chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
func: () => window.getSelection().toString()
});
if (results && results[0] && results[0].result) {
const text = results[0].result.trim();
if (text) {
await handleContextMenuSearch(text, tabs[0]);
}
}
} catch (error) {
console.error('Failed to get selection:', error);
}
}
});
}
});
// Show notification
async function showNotification(title, message) {
try {
await chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title,
message
});
} catch (error) {
console.error('Failed to show notification:', error);
}
}
// Clean up old tab groups (older than 24 hours)
async function cleanupOldGroups() {
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
for (const [groupId, info] of tabGroups.entries()) {
if (info.timestamp < oneDayAgo) {
tabGroups.delete(groupId);
}
}
// Schedule next cleanup
setTimeout(cleanupOldGroups, 60 * 60 * 1000); // Every hour
}
// Handle tab group removal
chrome.tabGroups.onRemoved.addListener((groupId) => {
tabGroups.delete(groupId);
});
// Handle tab removal from groups
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
// Clean up tab groups that have no tabs left
for (const [groupId, info] of tabGroups.entries()) {
const index = info.tabIds.indexOf(tabId);
if (index > -1) {
info.tabIds.splice(index, 1);
if (info.tabIds.length === 0) {
tabGroups.delete(groupId);
}
}
}
});
console.log('1SO SearchHub background service worker loaded');