diff --git a/background/service-worker.js b/background/service-worker.js
index 122a9ca..9cbff05 100644
--- a/background/service-worker.js
+++ b/background/service-worker.js
@@ -8,6 +8,17 @@ importScripts('../utils/rule-engine.js');
importScripts('../utils/rule-manager.js');
importScripts('./auto-grouper.js');
+// New modules for enhanced functionality
+importScripts('../utils/ml-classifier.js');
+importScripts('../utils/user-behavior-tracker.js');
+importScripts('../utils/smart-grouping-engine.js');
+importScripts('../utils/content-type-classifier.js');
+importScripts('../utils/scene-classifier.js');
+importScripts('../utils/scene-grouper.js');
+importScripts('../utils/group-cache-manager.js');
+importScripts('../utils/sync-manager.js');
+importScripts('../utils/multi-window-sync.js');
+
class SuperTabServiceWorker {
constructor() {
this.eventBus = new EventBus();
@@ -17,6 +28,18 @@ class SuperTabServiceWorker {
this.ruleEngine = null;
this.ruleManager = null;
this.autoGrouper = null;
+
+ // New module instances
+ this.mlClassifier = null;
+ this.userBehaviorTracker = null;
+ this.smartGroupingEngine = null;
+ this.contentTypeClassifier = null;
+ this.sceneClassifier = null;
+ this.sceneGrouper = null;
+ this.groupCacheManager = null;
+ this.syncManager = null;
+ this.multiWindowSync = null;
+
this.initialized = false;
this.initializationPromise = null;
this.tabWatcher = null;
@@ -54,13 +77,80 @@ class SuperTabServiceWorker {
this.ruleManager = this.tabManager.ruleManager || new RuleManager(this.storageManager);
this.autoGrouper = this.tabManager.autoGrouper || new AutoGrouper(this.tabManager, this.ruleEngine, this.ruleManager);
+ // Initialize new ML and smart grouping modules
+ console.log('🧠 Initializing ML and smart grouping modules...');
+
+ this.mlClassifier = new MLClassifier({
+ algorithm: 'naive-bayes',
+ learningRate: 0.1,
+ enableOnlineLearning: true
+ });
+ await this.mlClassifier.initialize();
+
+ this.userBehaviorTracker = new UserBehaviorTracker(this.storageManager, this.eventBus);
+ await this.userBehaviorTracker.initialize();
+
+ this.smartGroupingEngine = new SmartGroupingEngine(
+ this.storageManager,
+ this.eventBus,
+ {
+ mlClassifier: this.mlClassifier,
+ behaviorTracker: this.userBehaviorTracker,
+ useMLForDomain: true,
+ useMLForContent: true,
+ useBehaviorData: true
+ }
+ );
+ await this.smartGroupingEngine.initialize();
+
+ // Initialize scene-based grouping modules
+ console.log('🎬 Initializing scene-based grouping modules...');
+
+ this.contentTypeClassifier = new ContentTypeClassifier();
+ await this.contentTypeClassifier.initialize();
+
+ this.sceneClassifier = new SceneClassifier();
+ await this.sceneClassifier.initialize();
+
+ this.sceneGrouper = new SceneGrouper(
+ this.storageManager,
+ this.eventBus,
+ {
+ contentTypeClassifier: this.contentTypeClassifier,
+ sceneClassifier: this.sceneClassifier,
+ enableKeywordRules: true,
+ enableContentTypes: true,
+ enableScenes: true,
+ defaultGroupingPriority: ['keyword', 'custom', 'content', 'scene']
+ }
+ );
+ await this.sceneGrouper.initialize();
+
+ // Initialize cache and sync modules
+ console.log('💾 Initializing cache and sync modules...');
+
+ this.groupCacheManager = new GroupCacheManager(this.storageManager, this.eventBus);
+ await this.groupCacheManager.initialize();
+
+ this.syncManager = new SyncManager(this.storageManager, this.eventBus);
+ await this.syncManager.initialize();
+
+ this.multiWindowSync = new MultiWindowSync(this.storageManager, this.eventBus);
+ await this.multiWindowSync.initialize();
+
+ // Setup event connections between modules
+ this.setupModuleInterconnections();
+
this.setupDataSyncEvents();
this.initialized = true;
- console.log('✅ SuperTab Service Worker initialized successfully');
+ console.log('✅ SuperTab Service Worker initialized successfully with all new modules');
// Perform initial tab sync
await this.performInitialSync();
+
+ // Train ML model from existing data
+ await this.trainFromExistingData();
} catch (error) {
console.error('❌ Failed to initialize SuperTab Service Worker:', error);
throw error;
@@ -378,6 +468,239 @@ class SuperTabServiceWorker {
sendResponse({ success: true, data: metrics });
break;
+ // ========== New smart grouping actions ==========
+
+ case 'getSmartGrouping':
+ try {
+ const smartTabs = data.tabs || [];
+ const smartOptions = data.options || {};
+ const smartGroups = await this.getSmartGrouping(smartTabs, smartOptions);
+ sendResponse({ success: true, data: smartGroups });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'getSceneGrouping':
+ try {
+ const sceneTabs = data.tabs || [];
+ const sceneOptions = data.options || {};
+ const sceneGroups = await this.getSceneGrouping(sceneTabs, sceneOptions);
+ sendResponse({ success: true, data: sceneGroups });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ // ========== ML classifier actions ==========
+
+ case 'predictGroupForTab':
+ try {
+ if (!data?.tab) {
+ sendResponse({ success: false, error: 'tab is required' });
+ break;
+ }
+ const prediction = await this.predictGroupForTab(data.tab);
+ sendResponse({ success: true, data: prediction });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'learnFromUserAction':
+ try {
+ if (!data?.tabs || !Array.isArray(data.tabs) || !data?.groupName) {
+ sendResponse({ success: false, error: 'tabs array and groupName are required' });
+ break;
+ }
+ const learnResult = await this.learnFromUserAction(data.tabs, data.groupName, data.isManual !== false);
+ sendResponse(learnResult);
+ if (learnResult.success) {
+ this.scheduleSidebarRefresh('model_trained');
+ }
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'getMLStats':
+ try {
+ const mlStats = await this.getMLStats();
+ sendResponse({ success: true, data: mlStats });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'setMLAlgorithm':
+ try {
+ if (!data?.algorithm) {
+ sendResponse({ success: false, error: 'algorithm is required' });
+ break;
+ }
+ const algoResult = await this.setMLAlgorithm(data.algorithm);
+ sendResponse(algoResult);
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'resetMLModel':
+ try {
+ const resetResult = await this.resetMLModel();
+ sendResponse(resetResult);
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ // ========== Sync actions ==========
+
+ case 'forceSync':
+ try {
+ const syncResult = await this.forceSync();
+ sendResponse({ success: true, data: syncResult });
+ this.scheduleSidebarRefresh('sync_completed');
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'exportGroupingRules':
+ try {
+ const exportOptions = data?.options || {};
+ const exportData = await this.exportGroupingRules(exportOptions);
+ sendResponse({ success: true, data: exportData });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'importGroupingRules':
+ try {
+ if (!data?.data) {
+ sendResponse({ success: false, error: 'import data is required' });
+ break;
+ }
+ const importOptions = data?.options || {};
+ const importResult = await this.importGroupingRules(data.data, importOptions);
+ sendResponse(importResult);
+ if (importResult.success) {
+ this.scheduleSidebarRefresh('rules_imported');
+ }
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ // ========== Cache actions ==========
+
+ case 'invalidateCache':
+ try {
+ const invalidateResult = await this.invalidateCache();
+ sendResponse(invalidateResult);
+ this.scheduleSidebarRefresh('cache_invalidated');
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'getCacheStats':
+ try {
+ const cacheStats = await this.getCacheStats();
+ sendResponse({ success: true, data: cacheStats });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ // ========== Scene classifier actions ==========
+
+ case 'getContentTypes':
+ try {
+ const contentTypes = await this.getContentTypes();
+ sendResponse({ success: true, data: contentTypes });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'getScenes':
+ try {
+ const scenes = await this.getScenes();
+ sendResponse({ success: true, data: scenes });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'setScenePriority':
+ try {
+ if (!data?.sceneName || data?.priority == null) {
+ sendResponse({ success: false, error: 'sceneName and priority are required' });
+ break;
+ }
+ const scenePriorityResult = await this.setScenePriority(data.sceneName, data.priority);
+ sendResponse(scenePriorityResult);
+ this.scheduleSidebarRefresh('scene_priority_updated');
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'setGroupingMode':
+ try {
+ if (!data?.mode || data?.enabled == null) {
+ sendResponse({ success: false, error: 'mode and enabled are required' });
+ break;
+ }
+ const modeResult = await this.setGroupingMode(data.mode, data.enabled);
+ sendResponse(modeResult);
+ this.scheduleSidebarRefresh('grouping_mode_updated');
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'setGroupingPriority':
+ try {
+ if (!data?.mode || data?.priority == null) {
+ sendResponse({ success: false, error: 'mode and priority are required' });
+ break;
+ }
+ const priorityResult = await this.setGroupingPriority(data.mode, data.priority);
+ sendResponse(priorityResult);
+ this.scheduleSidebarRefresh('grouping_priority_updated');
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ case 'addKeywordRule':
+ try {
+ if (!data?.rule) {
+ sendResponse({ success: false, error: 'rule is required' });
+ break;
+ }
+ const keywordResult = await this.addKeywordRule(data.rule);
+ sendResponse(keywordResult);
+ this.scheduleSidebarRefresh('keyword_rule_added');
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
+ // ========== Multi-window actions ==========
+
+ case 'getMultiWindowStats':
+ try {
+ const multiWindowStats = await this.getMultiWindowStats();
+ sendResponse({ success: true, data: multiWindowStats });
+ } catch (error) {
+ sendResponse({ success: false, error: error.message });
+ }
+ break;
+
default:
sendResponse({ success: false, error: `Unknown action: ${action}` });
}
@@ -775,6 +1098,331 @@ class SuperTabServiceWorker {
return false;
}
}
+
+ // ========== New module helper methods ==========
+
+ setupModuleInterconnections() {
+ if (!this.eventBus) return;
+
+ // Connect ML classifier with behavior tracker
+ if (this.userBehaviorTracker && this.mlClassifier) {
+ this.userBehaviorTracker.on('training_data_ready', (data) => {
+ if (data && data.length > 0) {
+ this.mlClassifier.train(data);
+ }
+ });
+ }
+
+ // Connect multi-window sync with cache manager
+ if (this.multiWindowSync && this.groupCacheManager) {
+ this.multiWindowSync.on('remote_tab_moved', (payload) => {
+ this.groupCacheManager.handleTabMoved(
+ { id: payload.tab?.id, ...payload.tab },
+ payload.groupId
+ );
+ });
+
+ this.multiWindowSync.on('remote_group_created', (payload) => {
+ this.groupCacheManager.handleGroupCreated(payload.group);
+ });
+
+ this.multiWindowSync.on('remote_group_updated', (payload) => {
+ this.groupCacheManager.handleGroupUpdated(payload.group);
+ });
+
+ this.multiWindowSync.on('remote_group_deleted', (payload) => {
+ this.groupCacheManager.handleGroupDeleted(payload.groupId);
+ });
+ }
+
+ // Connect event bus with multi-window sync
+ this.eventBus.on('tab_created', (tabData) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyTabCreated(tabData);
+ }
+ });
+
+ this.eventBus.on('tab_removed', ({ tabId, tabData }) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyTabRemoved(tabId, tabData);
+ }
+ });
+
+ this.eventBus.on('tab_updated', (tabData) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyTabUpdated(tabData);
+ }
+ });
+
+ this.eventBus.on('tab_moved_to_group', ({ tab, groupId, oldGroupId }) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyTabMovedToGroup(tab, groupId, oldGroupId);
+ }
+ });
+
+ this.eventBus.on('group_created', (group) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyGroupCreated(group);
+ }
+ });
+
+ this.eventBus.on('group_updated', ({ group }) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyGroupUpdated(group);
+ }
+ });
+
+ this.eventBus.on('group_deleted', ({ groupId, tabIds }) => {
+ if (this.multiWindowSync) {
+ this.multiWindowSync.notifyGroupDeleted(groupId, tabIds);
+ }
+ });
+
+ console.log('🔗 Module interconnections setup complete');
+ }
+
+ async trainFromExistingData() {
+ try {
+ if (!this.userBehaviorTracker || !this.mlClassifier) {
+ console.log('⚠️ ML training modules not available, skipping training');
+ return;
+ }
+
+ console.log('🧠 Training ML model from existing data...');
+
+ const trainingData = await this.userBehaviorTracker.generateTrainingData();
+
+ if (trainingData && trainingData.length > 0) {
+ await this.mlClassifier.train(trainingData);
+ console.log(`✅ ML model trained with ${trainingData.length} samples`);
+
+ await this.mlClassifier.save();
+ console.log('💾 ML model saved to storage');
+ } else {
+ console.log('ℹ️ No training data available, using default model');
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to train ML model:', error);
+ }
+ }
+
+ // ========== Smart grouping methods ==========
+
+ async getSmartGrouping(tabs, options = {}) {
+ if (!this.smartGroupingEngine) {
+ throw new Error('Smart grouping engine not initialized');
+ }
+
+ const groupType = options.groupType || 'smart';
+ let groups;
+
+ switch (groupType) {
+ case 'domain':
+ groups = await this.smartGroupingEngine.groupByDomain(tabs, options);
+ break;
+ case 'date':
+ groups = await this.smartGroupingEngine.groupByDate(tabs, options);
+ break;
+ case 'content':
+ groups = await this.smartGroupingEngine.groupByContent(tabs, options);
+ break;
+ case 'smart':
+ default:
+ groups = await this.smartGroupingEngine.groupBySmartRules(tabs, [], options);
+ break;
+ }
+
+ return groups;
+ }
+
+ async getSceneGrouping(tabs, options = {}) {
+ if (!this.sceneGrouper) {
+ throw new Error('Scene grouper not initialized');
+ }
+
+ return await this.sceneGrouper.groupTabs(tabs, options);
+ }
+
+ // ========== ML prediction methods ==========
+
+ async predictGroupForTab(tab) {
+ if (!this.mlClassifier) {
+ throw new Error('ML classifier not initialized');
+ }
+
+ const prediction = this.mlClassifier.predict(tab);
+ return {
+ groupName: prediction.label,
+ confidence: prediction.confidence,
+ probabilities: prediction.probabilities
+ };
+ }
+
+ async learnFromUserAction(tabs, groupName, isManual = true) {
+ if (!this.userBehaviorTracker || !this.mlClassifier) {
+ return { success: false, error: 'Learning modules not available' };
+ }
+
+ // Track the user action
+ await this.userBehaviorTracker.learnManualPreference(tabs, groupName);
+
+ // Update ML model if this is a manual action
+ if (isManual && tabs.length > 0) {
+ for (const tab of tabs) {
+ await this.mlClassifier.learn(tab, groupName);
+ }
+ await this.mlClassifier.save();
+ }
+
+ return { success: true, learned: tabs.length };
+ }
+
+ // ========== Sync methods ==========
+
+ async forceSync() {
+ if (!this.syncManager) {
+ throw new Error('Sync manager not initialized');
+ }
+
+ return await this.syncManager.performSync(true);
+ }
+
+ async exportGroupingRules(options = {}) {
+ if (!this.syncManager) {
+ throw new Error('Sync manager not initialized');
+ }
+
+ return await this.syncManager.exportData(options);
+ }
+
+ async importGroupingRules(data, options = {}) {
+ if (!this.syncManager) {
+ throw new Error('Sync manager not initialized');
+ }
+
+ return await this.syncManager.importData(data, options);
+ }
+
+ // ========== Cache methods ==========
+
+ async invalidateCache() {
+ if (!this.groupCacheManager) {
+ throw new Error('Cache manager not initialized');
+ }
+
+ this.groupCacheManager.invalidateCache('manual');
+ return { success: true };
+ }
+
+ async getCacheStats() {
+ if (!this.groupCacheManager) {
+ throw new Error('Cache manager not initialized');
+ }
+
+ return this.groupCacheManager.getStats();
+ }
+
+ // ========== Scene classifier methods ==========
+
+ async getContentTypes() {
+ if (!this.contentTypeClassifier) {
+ throw new Error('Content type classifier not initialized');
+ }
+
+ return this.contentTypeClassifier.getCategories();
+ }
+
+ async getScenes() {
+ if (!this.sceneClassifier) {
+ throw new Error('Scene classifier not initialized');
+ }
+
+ return this.sceneClassifier.getScenes();
+ }
+
+ async setScenePriority(sceneName, priority) {
+ if (!this.sceneClassifier) {
+ throw new Error('Scene classifier not initialized');
+ }
+
+ this.sceneClassifier.setScenePriority(sceneName, priority);
+ return { success: true };
+ }
+
+ async setGroupingMode(mode, enabled) {
+ if (!this.sceneGrouper) {
+ throw new Error('Scene grouper not initialized');
+ }
+
+ this.sceneGrouper.setGroupingMode(mode, enabled);
+ return { success: true };
+ }
+
+ async setGroupingPriority(mode, priority) {
+ if (!this.sceneGrouper) {
+ throw new Error('Scene grouper not initialized');
+ }
+
+ this.sceneGrouper.setGroupingPriority(mode, priority);
+ return { success: true };
+ }
+
+ async addKeywordRule(rule) {
+ if (!this.sceneGrouper) {
+ throw new Error('Scene grouper not initialized');
+ }
+
+ this.sceneGrouper.addKeywordRule(rule);
+ return { success: true };
+ }
+
+ // ========== Multi-window methods ==========
+
+ async getMultiWindowStats() {
+ if (!this.multiWindowSync) {
+ throw new Error('Multi-window sync not initialized');
+ }
+
+ return this.multiWindowSync.getStats();
+ }
+
+ // ========== ML classifier methods ==========
+
+ async setMLAlgorithm(algorithm) {
+ if (!this.mlClassifier) {
+ throw new Error('ML classifier not initialized');
+ }
+
+ const validAlgorithms = ['naive-bayes', 'logistic-regression', 'decision-tree'];
+ if (!validAlgorithms.includes(algorithm)) {
+ throw new Error(`Invalid algorithm. Must be one of: ${validAlgorithms.join(', ')}`);
+ }
+
+ this.mlClassifier.algorithm = algorithm;
+ return { success: true, algorithm };
+ }
+
+ async resetMLModel() {
+ if (!this.mlClassifier) {
+ throw new Error('ML classifier not initialized');
+ }
+
+ this.mlClassifier.reset();
+ return { success: true };
+ }
+
+ async getMLStats() {
+ if (!this.mlClassifier) {
+ throw new Error('ML classifier not initialized');
+ }
+
+ return {
+ algorithm: this.mlClassifier.algorithm,
+ trainingCount: this.mlClassifier.trainingData?.length || 0,
+ isTrained: this.mlClassifier.isTrained,
+ hasModel: this.mlClassifier.hasModel
+ };
+ }
}
// Global service worker instance
diff --git a/manifest.json b/manifest.json
index a9cb5ee..9934e0d 100644
--- a/manifest.json
+++ b/manifest.json
@@ -8,12 +8,24 @@
"storage",
"sidePanel",
"contextMenus",
- "bookmarks"
+ "bookmarks",
+ "windows",
+ "alarms",
+ "unlimitedStorage"
],
"host_permissions": [
"http://*/*",
"https://*/*"
],
+ "content_security_policy": {
+ "extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; connect-src 'self' chrome-extension://*;"
+ },
+ "cross_origin_embedder_policy": {
+ "value": "require-corp"
+ },
+ "cross_origin_opener_policy": {
+ "value": "same-origin"
+ },
"background": {
"service_worker": "background/service-worker.js"
},
diff --git a/ui/rules/rules.html b/ui/rules/rules.html
index 994e9eb..dbe3cc7 100644
--- a/ui/rules/rules.html
+++ b/ui/rules/rules.html
@@ -4,6 +4,9 @@
+
+
+
SuperTab - 规则管理
diff --git a/ui/settings/settings.html b/ui/settings/settings.html
index 02fdc8c..4f58d9d 100644
--- a/ui/settings/settings.html
+++ b/ui/settings/settings.html
@@ -4,6 +4,9 @@
+
+
+
SuperTab - 设置
diff --git a/ui/sidebar/sidebar.html b/ui/sidebar/sidebar.html
index d7ad20b..1d0dbdb 100644
--- a/ui/sidebar/sidebar.html
+++ b/ui/sidebar/sidebar.html
@@ -3,6 +3,9 @@
+
+
+
SuperTab
diff --git a/utils/content-type-classifier.js b/utils/content-type-classifier.js
new file mode 100644
index 0000000..22717fc
--- /dev/null
+++ b/utils/content-type-classifier.js
@@ -0,0 +1,1856 @@
+/**
+ * ContentTypeClassifier - 内容类型分类器
+ *
+ * 识别网页内容类型:
+ * - 文档 (PDF, Word, 文档类网站)
+ * - 视频 (视频网站, 直播平台)
+ * - 图片 (图片网站, 社交媒体图片)
+ * - 办公工具 (OA, 协作工具, 邮箱)
+ * - 开发工具 (代码仓库, API文档, 开发平台)
+ * - 社交网络 (社交媒体, 聊天工具)
+ * - 购物电商
+ * - 新闻资讯
+ * - 搜索引擎
+ */
+
+class ContentTypeClassifier {
+ constructor(options = {}) {
+ this.customPatterns = new Map();
+ this.customWeights = options.customWeights || {};
+
+ this.defaultWeights = {
+ domain: 0.5,
+ path: 0.25,
+ title: 0.2,
+ query: 0.05
+ };
+
+ this.contentTypePatterns = {
+ document: {
+ name: '文档',
+ icon: '📄',
+ color: '#4CAF50',
+ domains: [
+ 'docs.google.com',
+ 'drive.google.com',
+ 'onedrive.live.com',
+ 'sharepoint.com',
+ 'dropbox.com',
+ 'box.com',
+ 'notion.so',
+ 'evernote.com',
+ 'confluence.atlassian.com',
+ 'slite.com',
+ 'quip.com',
+ 'miro.com',
+ 'figma.com',
+ 'canva.com',
+ 'adobe.com',
+ 'office.com',
+ 'office365.com',
+ 'docsend.com',
+ 'scribd.com',
+ 'slideshare.net',
+ 'prezi.com',
+ 'pdfdrive.com',
+ 'academia.edu',
+ 'researchgate.net',
+ 'ieee.org',
+ 'acm.org',
+ 'arxiv.org',
+ 'zhihu.com',
+ 'zhuanlan.zhihu.com',
+ 'jianshu.com',
+ 'csdn.net',
+ 'cnblogs.com',
+ 'segmentfault.com',
+ 'juejin.cn',
+ 'oschina.net',
+ 'bookstack.cn',
+ 'yuque.com',
+ 'wolai.com',
+ 'feishu.cn',
+ 'dingtalk.com',
+ 'wework.cn'
+ ],
+ pathPatterns: [
+ '/doc',
+ '/document',
+ '/docs',
+ '/pdf',
+ '/article',
+ '/blog',
+ '/post',
+ '/wiki',
+ '/help',
+ '/support',
+ '/knowledge',
+ '/guide',
+ '/tutorial',
+ '/learn',
+ '/course',
+ '/ebook',
+ '/book',
+ '/notes',
+ '/notebook',
+ '/whitepaper',
+ '/report',
+ '/case-study',
+ '/research'
+ ],
+ titleKeywords: [
+ 'pdf', '文档', '报告', '论文', '文章', '博客', '教程',
+ '指南', '帮助', 'wiki', '百科', '笔记', 'notebook',
+ '电子书', 'ebook', '白皮书', 'whitepaper', '研究',
+ '案例', 'case study', '文档中心', '知识库'
+ ],
+ fileExtensions: [
+ '.pdf', '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx',
+ '.txt', '.rtf', '.odt', '.ods', '.odp', '.epub', '.mobi'
+ ]
+ },
+
+ video: {
+ name: '视频',
+ icon: '🎬',
+ color: '#FF5722',
+ domains: [
+ 'youtube.com',
+ 'youtu.be',
+ 'vimeo.com',
+ 'dailymotion.com',
+ 'twitch.tv',
+ 'netflix.com',
+ 'hulu.com',
+ 'disneyplus.com',
+ 'hbo.com',
+ 'amazon.com',
+ 'primevideo.com',
+ 'bilibili.com',
+ 'bilibili.cn',
+ 'iqiyi.com',
+ 'youku.com',
+ 'tudou.com',
+ 'mgtv.com',
+ 'le.com',
+ 'pptv.com',
+ 'sohu.com',
+ 'ifeng.com',
+ 'tencent.com',
+ 'v.qq.com',
+ 'live.qq.com',
+ 'douyin.com',
+ 'tiktok.com',
+ 'kuaishou.com',
+ 'xiaohongshu.com',
+ 'weibo.com',
+ 'weibo.cn',
+ 'huya.com',
+ 'douyu.com',
+ 'yy.com',
+ 'inke.cn',
+ 'huajiao.com',
+ 'bigo.tv',
+ 'pearvideo.com',
+ 'thepaper.cn',
+ 'ixigua.com',
+ 'toutiao.com',
+ '36kr.com',
+ 'zhihu.com',
+ 'ted.com',
+ 'udemy.com',
+ 'coursera.org',
+ 'edX.org',
+ 'lynda.com',
+ 'pluralsight.com',
+ 'skillshare.com',
+ 'masterclass.com'
+ ],
+ pathPatterns: [
+ '/watch',
+ '/video',
+ '/videos',
+ '/v/',
+ '/live',
+ '/stream',
+ '/broadcast',
+ '/tv',
+ '/movie',
+ '/movies',
+ '/film',
+ '/show',
+ '/episode',
+ '/clip',
+ '/reel',
+ '/shorts',
+ '/tiktok',
+ '/playlist',
+ '/channel'
+ ],
+ titleKeywords: [
+ '视频', '直播', '电影', '电视剧', '综艺', '动漫',
+ 'watch', 'video', 'live', 'stream', 'movie', 'film',
+ 'episode', 'season', '预告片', '预告', 'trailer',
+ '教程', 'tutorial', '课程', 'course', 'lecture'
+ ],
+ queryPatterns: [
+ 'v=',
+ 'video_id=',
+ 'vid='
+ ]
+ },
+
+ image: {
+ name: '图片',
+ icon: '🖼️',
+ color: '#E91E63',
+ domains: [
+ 'unsplash.com',
+ 'pexels.com',
+ 'pixabay.com',
+ 'shutterstock.com',
+ 'istockphoto.com',
+ 'gettyimages.com',
+ 'adobe.com',
+ 'stock.adobe.com',
+ 'flickr.com',
+ '500px.com',
+ 'instagram.com',
+ 'pinterest.com',
+ 'tumblr.com',
+ 'deviantart.com',
+ 'artstation.com',
+ 'dribbble.com',
+ 'behance.net',
+ 'imgur.com',
+ 'giphy.com',
+ 'tenor.com',
+ 'reddit.com',
+ 'imgflip.com',
+ 'canva.com',
+ 'figma.com',
+ 'photopea.com',
+ 'pixlr.com',
+ 'fotor.com',
+ 'baike.baidu.com',
+ 'image.baidu.com',
+ 'pic.sogou.com',
+ 'images.so.com',
+ 'bing.com',
+ 'google.com',
+ 'huaban.com',
+ '699pic.com',
+ 'ibaotu.com',
+ '58pic.com',
+ '90design.com',
+ 'fotor.com.cn',
+ 'gracg.com',
+ 'zcool.com.cn',
+ 'ui.cn',
+ 'uisdc.com',
+ 'xueui.cn'
+ ],
+ pathPatterns: [
+ '/image',
+ '/images',
+ '/img',
+ '/photo',
+ '/photos',
+ '/gallery',
+ '/album',
+ '/wallpaper',
+ '/background',
+ '/icon',
+ '/icons',
+ '/svg',
+ '/png',
+ '/jpg',
+ '/jpeg',
+ '/gif',
+ '/artwork',
+ '/design',
+ '/illustration',
+ '/drawing',
+ '/sketch'
+ ],
+ titleKeywords: [
+ '图片', '照片', '图像', '壁纸', '图标', '设计',
+ '插画', '艺术', '摄影', 'gallery', 'photo', 'image',
+ 'wallpaper', 'icon', 'design', 'art', 'illustration',
+ '手绘', '漫画', 'anime', 'manga', '二次元'
+ ],
+ fileExtensions: [
+ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp',
+ '.svg', '.ico', '.tif', '.tiff', '.raw', '.psd',
+ '.ai', '.eps', '.pdf', '.xcf'
+ ]
+ },
+
+ office: {
+ name: '办公工具',
+ icon: '💼',
+ color: '#2196F3',
+ domains: [
+ 'office.com',
+ 'office365.com',
+ 'microsoft365.com',
+ 'outlook.com',
+ 'outlook.office.com',
+ 'gmail.com',
+ 'mail.google.com',
+ 'yahoo.com',
+ 'hotmail.com',
+ 'live.com',
+ 'protonmail.com',
+ 'tutanota.com',
+ 'fastmail.com',
+ 'zoom.us',
+ 'teams.microsoft.com',
+ 'slack.com',
+ 'discord.com',
+ 'meet.google.com',
+ 'webex.com',
+ 'gotomeeting.com',
+ 'jira.atlassian.com',
+ 'trello.com',
+ 'asana.com',
+ 'notion.so',
+ 'basecamp.com',
+ 'clickup.com',
+ 'monday.com',
+ 'wrike.com',
+ 'todoist.com',
+ 'ticktick.com',
+ 'any.do',
+ 'rememberthemilk.com',
+ 'calendly.com',
+ 'doodle.com',
+ 'xmail.com',
+ '139.com',
+ '189.cn',
+ 'wo.cn',
+ '163.com',
+ 'mail.163.com',
+ '126.com',
+ 'yeah.net',
+ 'qq.com',
+ 'mail.qq.com',
+ 'foxmail.com',
+ 'sina.com.cn',
+ 'mail.sina.com.cn',
+ 'sohu.com',
+ 'vip.sohu.com',
+ '21cn.com',
+ '189.cn',
+ 'feishu.cn',
+ 'feishu.com',
+ 'larksuite.com',
+ 'dingtalk.com',
+ 'taobao.com',
+ 'tmall.com',
+ 'alipay.com',
+ '钉钉',
+ 'wework.cn',
+ '企业微信',
+ 'qy.weixin.qq.com',
+ 'work.weixin.qq.com',
+ 'wps.cn',
+ 'wps.com',
+ 'kdocs.cn',
+ 'yunshangxiezuo.com',
+ 'teambition.com',
+ 'coding.net',
+ 'gitee.com',
+ 'gitlab.com',
+ 'github.com',
+ 'bitbucket.org',
+ 'azure.com',
+ 'aws.amazon.com',
+ 'cloud.tencent.com',
+ 'aliyun.com',
+ 'huaweicloud.com',
+ 'baidu.com',
+ 'cloud.google.com'
+ ],
+ pathPatterns: [
+ '/mail',
+ '/email',
+ '/inbox',
+ '/message',
+ '/messages',
+ '/chat',
+ '/meeting',
+ '/meet',
+ '/calendar',
+ '/cal',
+ '/task',
+ '/tasks',
+ '/todo',
+ '/project',
+ '/projects',
+ '/board',
+ '/workspace',
+ '/team',
+ '/collab',
+ '/collaboration',
+ '/office',
+ '/work'
+ ],
+ titleKeywords: [
+ '邮箱', '邮件', 'email', 'mail', 'inbox',
+ '会议', 'meeting', 'zoom', 'teams', 'slack', 'discord',
+ '日程', 'calendar', '任务', 'task', 'todo',
+ '项目', 'project', '协作', 'collaboration',
+ '办公', 'office', '工作', 'work', '团队', 'team'
+ ]
+ },
+
+ development: {
+ name: '开发工具',
+ icon: '💻',
+ color: '#673AB7',
+ domains: [
+ 'github.com',
+ 'gitlab.com',
+ 'bitbucket.org',
+ 'gitee.com',
+ 'coding.net',
+ 'gitcode.com',
+ 'atom.io',
+ 'code.visualstudio.com',
+ 'vscode.dev',
+ 'codesandbox.io',
+ 'codepen.io',
+ 'jsfiddle.net',
+ 'stackblitz.com',
+ 'repl.it',
+ 'glitch.com',
+ 'dev.tencent.com',
+ 'cloud.tencent.com',
+ 'developer.aliyun.com',
+ 'huaweicloud.com',
+ 'console.aws.amazon.com',
+ 'portal.azure.com',
+ 'console.cloud.google.com',
+ 'vercel.com',
+ 'netlify.com',
+ 'heroku.com',
+ 'digitalocean.com',
+ 'linode.com',
+ 'docker.com',
+ 'hub.docker.com',
+ 'kubernetes.io',
+ 'stackoverflow.com',
+ 'stackexchange.com',
+ 'serverfault.com',
+ 'superuser.com',
+ 'github.io',
+ 'npmjs.com',
+ 'npmjs.org',
+ 'yarnpkg.com',
+ 'pip.pypa.io',
+ 'pypi.org',
+ 'rubygems.org',
+ 'crates.io',
+ 'pub.dev',
+ 'maven.apache.org',
+ 'mvnrepository.com',
+ 'nuget.org',
+ 'packagist.org',
+ 'go.dev',
+ 'pkg.go.dev',
+ 'deno.land',
+ 'nodejs.org',
+ 'python.org',
+ 'java.com',
+ 'oracle.com',
+ 'jetbrains.com',
+ 'code.visualstudio.com',
+ 'atom.io',
+ 'sublimetext.com',
+ 'vim.org',
+ 'neovim.io',
+ 'emacs.org',
+ 'developer.mozilla.org',
+ 'mdn.io',
+ 'w3schools.com',
+ 'caniuse.com',
+ 'caniuse.dev',
+ 'jsfiddle.net',
+ 'babeljs.io',
+ 'webpack.js.org',
+ 'vitejs.dev',
+ 'rollupjs.org',
+ 'parceljs.org',
+ 'jestjs.io',
+ 'testing-library.com',
+ 'cypress.io',
+ 'playwright.dev',
+ 'postman.com',
+ 'insomnia.rest',
+ 'swagger.io',
+ 'openapi.org',
+ 'graphql.org',
+ 'apollographql.com',
+ 'mongodb.com',
+ 'mongodb.org',
+ 'mysql.com',
+ 'postgresql.org',
+ 'redis.io',
+ 'elasticsearch.org',
+ 'kibana.co',
+ 'logstash.net',
+ 'prometheus.io',
+ 'grafana.com',
+ 'datadoghq.com',
+ 'newrelic.com',
+ 'sentry.io',
+ 'bugsnag.com',
+ 'segment.com',
+ 'segment.io',
+ 'amplitude.com',
+ 'mixpanel.com',
+ 'hotjar.com',
+ 'optimizely.com',
+ 'launchdarkly.com',
+ 'juejin.cn',
+ 'segmentfault.com',
+ 'oschina.net',
+ 'cnblogs.com',
+ 'csdn.net',
+ 'v2ex.com',
+ 'zhihu.com',
+ 'ruby-china.org',
+ 'golangtc.com',
+ 'nodejs.cn',
+ 'webpack.docschina.org',
+ 'react.docschina.org',
+ 'vuejs.org',
+ 'cn.vuejs.org',
+ 'angular.io',
+ 'angular.cn',
+ 'svelte.dev',
+ 'reactjs.org',
+ 'react.dev',
+ 'nextjs.org',
+ 'nuxt.com',
+ 'gatsbyjs.com',
+ 'remix.run'
+ ],
+ pathPatterns: [
+ '/code',
+ '/src',
+ '/source',
+ '/repo',
+ '/repository',
+ '/project',
+ '/api',
+ '/docs',
+ '/documentation',
+ '/dev',
+ '/developer',
+ '/developers',
+ '/console',
+ '/dashboard',
+ '/admin',
+ '/config',
+ '/settings',
+ '/pipeline',
+ '/ci',
+ '/cd',
+ '/build',
+ '/deploy',
+ '/test',
+ '/debug',
+ '/log',
+ '/logs',
+ '/monitor',
+ '/metrics',
+ '/trace',
+ '/issue',
+ '/issues',
+ '/pull',
+ '/pr',
+ '/merge',
+ '/commit',
+ '/branch',
+ '/tag',
+ '/release'
+ ],
+ titleKeywords: [
+ '代码', '源码', 'code', 'source', 'repo', 'repository',
+ '开发', 'developer', 'development', 'dev',
+ 'api', '文档', 'docs', 'documentation',
+ '测试', 'test', '调试', 'debug',
+ '部署', 'deploy', '构建', 'build',
+ 'git', 'github', 'gitlab', 'gitee',
+ '框架', 'framework', '库', 'library',
+ 'package', 'npm', 'pip', 'maven', 'gradle',
+ 'docker', 'kubernetes', 'k8s', '容器',
+ '服务器', 'server', '云服务', 'cloud',
+ '数据库', 'database', 'mysql', 'postgres', 'redis',
+ '监控', 'monitor', '日志', 'log',
+ '错误', 'error', 'bug', 'issue'
+ ]
+ },
+
+ social: {
+ name: '社交网络',
+ icon: '👥',
+ color: '#00BCD4',
+ domains: [
+ 'facebook.com',
+ 'fb.com',
+ 'twitter.com',
+ 'x.com',
+ 'instagram.com',
+ 'linkedin.com',
+ 'pinterest.com',
+ 'tumblr.com',
+ 'reddit.com',
+ 'snapchat.com',
+ 'tiktok.com',
+ 'douyin.com',
+ 'weibo.com',
+ 'weibo.cn',
+ 'qq.com',
+ 'qzone.qq.com',
+ 'weixin.qq.com',
+ 'zhihu.com',
+ 'xiaohongshu.com',
+ 'bilibili.com',
+ 'tieba.baidu.com',
+ 'mafengwo.cn',
+ 'douban.com',
+ 'fanfou.com',
+ 'jianshu.com',
+ 'lofter.com',
+ 'yizhetuan.com',
+ 'kandian.com',
+ 'toutiao.com',
+ 'ixigua.com',
+ 'kuaishou.com',
+ 'huoshan.com',
+ 'meipai.com',
+ 'xiaoying.com',
+ 'yue365.com',
+ 'mogujie.com',
+ 'meilishuo.com',
+ 'alibaba.com',
+ 'taobao.com',
+ 'tmall.com',
+ 'jd.com',
+ 'pinduoduo.com',
+ 'suning.com',
+ 'gome.com.cn',
+ 'dangdang.com',
+ 'amazon.com',
+ 'ebay.com',
+ 'walmart.com',
+ 'target.com',
+ 'etsy.com',
+ 'shopify.com',
+ 'discord.com',
+ 'slack.com',
+ 'telegram.org',
+ 'telegram.com',
+ 'whatsapp.com',
+ 'wechat.com',
+ 'line.me',
+ 'kakaotalk.com',
+ 'signal.org',
+ 'wire.com',
+ 'matrix.org',
+ 'element.io'
+ ],
+ pathPatterns: [
+ '/user',
+ '/users',
+ '/profile',
+ '/u/',
+ '/@',
+ '/follow',
+ '/following',
+ '/follower',
+ '/followers',
+ '/friend',
+ '/friends',
+ '/post',
+ '/posts',
+ '/status',
+ '/statuses',
+ '/tweet',
+ '/tweets',
+ '/feed',
+ '/timeline',
+ '/message',
+ '/messages',
+ '/chat',
+ '/chats',
+ '/dm',
+ '/inbox',
+ '/notification',
+ '/notifications',
+ '/mention',
+ '/mentions',
+ '/comment',
+ '/comments',
+ '/like',
+ '/likes',
+ '/share',
+ '/shares',
+ '/repost',
+ '/retweet'
+ ],
+ titleKeywords: [
+ '用户', 'profile', 'user', '个人主页',
+ '关注', 'follow', '粉丝', 'follower', '好友', 'friend',
+ '动态', 'post', 'status', 'tweet', 'feed', 'timeline',
+ '消息', 'message', '聊天', 'chat', '私信', 'dm',
+ '通知', 'notification', '提醒', 'mention',
+ '评论', 'comment', '点赞', 'like', '分享', 'share',
+ '转发', 'repost', 'retweet', '收藏', '收藏夹',
+ '社交', 'social', '社区', 'community', '论坛', 'forum'
+ ]
+ },
+
+ shopping: {
+ name: '购物电商',
+ icon: '🛒',
+ color: '#FF9800',
+ domains: [
+ 'taobao.com',
+ 'tmall.com',
+ 'jd.com',
+ 'pinduoduo.com',
+ 'yangkeduo.com',
+ 'suning.com',
+ 'gome.com.cn',
+ 'dangdang.com',
+ 'amazon.com',
+ 'amazon.cn',
+ 'ebay.com',
+ 'walmart.com',
+ 'target.com',
+ 'bestbuy.com',
+ 'etsy.com',
+ 'shopify.com',
+ 'aliexpress.com',
+ 'alibaba.com',
+ '1688.com',
+ 'dhgate.com',
+ 'lightinthebox.com',
+ 'shein.com',
+ 'romwe.com',
+ 'asos.com',
+ 'zara.com',
+ 'hm.com',
+ 'uniqlo.com',
+ 'gap.com',
+ 'nike.com',
+ 'adidas.com',
+ 'puma.com',
+ 'underarmour.com',
+ 'apple.com',
+ 'microsoft.com',
+ 'samsung.com',
+ 'huawei.com',
+ 'xiaomi.com',
+ 'mi.com',
+ 'oppo.com',
+ 'vivo.com',
+ 'oneplus.com',
+ 'realme.com',
+ 'dell.com',
+ 'hp.com',
+ 'lenovo.com',
+ 'asus.com',
+ 'acer.com',
+ 'msi.com',
+ 'gigabyte.com',
+ 'evga.com',
+ 'corsair.com',
+ 'logitech.com',
+ 'razer.com',
+ 'steelseries.com',
+ 'coolermaster.com',
+ 'thermaltake.com',
+ 'nzxt.com',
+ 'lian-li.com',
+ 'phoenix.com',
+ 'jd.hk',
+ 'kaola.com',
+ 'netease.com',
+ 'you.163.com',
+ 'xiaomiyoupin.com',
+ 'mi.com',
+ 'suning.com',
+ 'gome.com.cn',
+ 'dangdang.com',
+ 'amazon.cn',
+ 'amazon.com',
+ 'walmart.com',
+ 'target.com',
+ 'costco.com',
+ 'costco.com.cn',
+ 'samsclub.com',
+ 'samsclub.cn',
+ 'carrefour.com',
+ 'carrefour.com.cn',
+ 'rt-mart.com.cn',
+ '永辉超市',
+ 'yonghui.com.cn',
+ 'meituan.com',
+ 'waimai.meituan.com',
+ 'dianping.com',
+ 'ele.me',
+ 'koubei.com',
+ 'fliggy.com',
+ 'ctrip.com',
+ 'qunar.com',
+ 'tuniu.com',
+ 'lvmama.com',
+ 'mafengwo.cn',
+ 'qunar.com',
+ 'ctrip.com',
+ 'booking.com',
+ 'airbnb.com',
+ 'airbnb.cn',
+ 'trip.com',
+ 'skyscanner.com',
+ 'expedia.com',
+ 'agoda.com',
+ 'hotels.com'
+ ],
+ pathPatterns: [
+ '/product',
+ '/products',
+ '/item',
+ '/items',
+ '/goods',
+ '/sku',
+ '/detail',
+ '/details',
+ '/category',
+ '/categories',
+ '/catalog',
+ '/shop',
+ '/store',
+ '/cart',
+ '/checkout',
+ '/order',
+ '/orders',
+ '/buy',
+ '/purchase',
+ '/pay',
+ '/payment',
+ '/coupon',
+ '/coupons',
+ '/discount',
+ '/sale',
+ '/promotion',
+ '/deal',
+ '/flash-sale',
+ '/seckill',
+ '/search',
+ '/list',
+ '/brand',
+ '/brands',
+ '/seller',
+ '/store'
+ ],
+ titleKeywords: [
+ '商品', 'product', 'item', 'goods', 'sku',
+ '详情', 'detail', '分类', 'category', 'catalog',
+ '购物车', 'cart', '结算', 'checkout',
+ '订单', 'order', '购买', 'buy', 'purchase',
+ '支付', 'pay', 'payment', '付款',
+ '优惠', 'discount', 'coupon', '促销', 'promotion',
+ '特价', 'sale', '秒杀', 'seckill', 'flash sale',
+ '品牌', 'brand', '店铺', 'store', 'shop',
+ '卖家', 'seller', '搜索', 'search',
+ '电商', 'e-commerce', 'shopping', '购物'
+ ]
+ },
+
+ news: {
+ name: '新闻资讯',
+ icon: '📰',
+ color: '#795548',
+ domains: [
+ 'bbc.com',
+ 'bbc.co.uk',
+ 'cnn.com',
+ 'foxnews.com',
+ 'nbcnews.com',
+ 'abcnews.go.com',
+ 'cbsnews.com',
+ 'msnbc.com',
+ 'bloomberg.com',
+ 'reuters.com',
+ 'ap.org',
+ 'apnews.com',
+ 'nytimes.com',
+ 'washingtonpost.com',
+ 'wsj.com',
+ 'ft.com',
+ 'economist.com',
+ 'time.com',
+ 'newsweek.com',
+ 'usatoday.com',
+ 'theguardian.com',
+ 'independent.co.uk',
+ 'telegraph.co.uk',
+ 'thetimes.co.uk',
+ 'scmp.com',
+ 'reuters.com',
+ 'bloomberg.com',
+ 'forbes.com',
+ 'fortune.com',
+ 'businessinsider.com',
+ 'cnbc.com',
+ 'marketwatch.com',
+ 'seekingalpha.com',
+ 'investopedia.com',
+ 'yahoo.com',
+ 'yahoo.co.jp',
+ 'google.com/news',
+ 'news.google.com',
+ 'apple.com/newsroom',
+ 'microsoft.com/en-us/newsroom',
+ 'techcrunch.com',
+ 'theverge.com',
+ 'engadget.com',
+ 'gizmodo.com',
+ 'wired.com',
+ 'arstechnica.com',
+ 'slashdot.org',
+ ' zdnet.com',
+ 'cnet.com',
+ 'pcmag.com',
+ 'tomshardware.com',
+ 'anandtech.com',
+ 'hexus.net',
+ 'bit-tech.net',
+ 'tencent.com',
+ 'qq.com',
+ 'news.qq.com',
+ 'finance.qq.com',
+ 'sports.qq.com',
+ 'ent.qq.com',
+ 'tech.qq.com',
+ 'auto.qq.com',
+ 'house.qq.com',
+ 'game.qq.com',
+ 'mil.qq.com',
+ 'news.qq.com',
+ 'sina.com.cn',
+ 'news.sina.com.cn',
+ 'finance.sina.com.cn',
+ 'sports.sina.com.cn',
+ 'ent.sina.com.cn',
+ 'tech.sina.com.cn',
+ 'auto.sina.com.cn',
+ 'house.sina.com.cn',
+ 'game.sina.com.cn',
+ 'mil.news.sina.com.cn',
+ 'sohu.com',
+ 'news.sohu.com',
+ 'finance.sohu.com',
+ 'sports.sohu.com',
+ 'yule.sohu.com',
+ 'it.sohu.com',
+ 'auto.sohu.com',
+ 'house.sohu.com',
+ 'game.sohu.com',
+ 'mil.sohu.com',
+ '163.com',
+ 'news.163.com',
+ 'money.163.com',
+ 'sports.163.com',
+ 'ent.163.com',
+ 'tech.163.com',
+ 'auto.163.com',
+ 'house.163.com',
+ 'game.163.com',
+ 'war.163.com',
+ 'ifeng.com',
+ 'news.ifeng.com',
+ 'finance.ifeng.com',
+ 'sports.ifeng.com',
+ 'ent.ifeng.com',
+ 'tech.ifeng.com',
+ 'auto.ifeng.com',
+ 'house.ifeng.com',
+ 'game.ifeng.com',
+ 'mil.ifeng.com',
+ 'thepaper.cn',
+ 'pengpai.cn',
+ 'guancha.cn',
+ 'chinanews.com',
+ 'news.cn',
+ 'people.com.cn',
+ 'xinhuanet.com',
+ 'cctv.com',
+ 'tv.cctv.com',
+ 'news.cctv.com',
+ 'huanqiu.com',
+ 'huanqiu.com',
+ 'globaltimes.cn',
+ 'chinadaily.com.cn',
+ 'ecns.cn',
+ 'caixin.com',
+ 'yicai.com',
+ '10jqka.com.cn',
+ 'eastmoney.com',
+ 'stockstar.com',
+ 'hexun.com',
+ 'jrj.com',
+ 'xueqiu.com',
+ 'gupiao.eastmoney.com',
+ '36kr.com',
+ 'huxiu.com',
+ 'iyiou.com',
+ 'tmtpost.com',
+ 'leiphone.com',
+ 'pingwest.com',
+ 'geekpark.net',
+ 'ifanr.com',
+ 'zhidx.com',
+ 'techweb.com.cn',
+ 'cnbeta.com',
+ 'itbear.com.cn',
+ 'pconline.com.cn',
+ 'zol.com.cn',
+ 'pcpop.com',
+ 'ithome.com',
+ 'mydrivers.com',
+ 'fastcompany.cn',
+ 'woshipm.com',
+ 'chanjet.com',
+ 'fanli.com',
+ 'smzdm.com',
+ '什么值得买',
+ 'duokan.com',
+ 'jingjiguancha.com',
+ 'caijing.com.cn',
+ 'yicai.com',
+ 'ce.cn',
+ 'people.com.cn',
+ 'xinhuanet.com',
+ 'china.com.cn',
+ 'huanqiu.com'
+ ],
+ pathPatterns: [
+ '/news',
+ '/article',
+ '/articles',
+ '/story',
+ '/stories',
+ '/report',
+ '/reports',
+ '/headline',
+ '/headlines',
+ '/breaking',
+ '/live',
+ '/update',
+ '/updates',
+ '/coverage',
+ '/analysis',
+ '/opinion',
+ '/editorial',
+ '/column',
+ '/columns',
+ '/blog',
+ '/blogs',
+ '/post',
+ '/posts',
+ '/topic',
+ '/topics',
+ '/subject',
+ '/feature',
+ '/features',
+ '/magazine',
+ '/issue',
+ '/edition',
+ '/finance',
+ '/business',
+ '/economy',
+ '/market',
+ '/stock',
+ '/sports',
+ '/entertainment',
+ '/ent',
+ '/tech',
+ '/technology',
+ '/auto',
+ '/lifestyle',
+ '/health',
+ '/science',
+ '/education',
+ '/politics',
+ '/world',
+ '/international',
+ '/china',
+ '/local'
+ ],
+ titleKeywords: [
+ '新闻', 'news', '报道', 'report', 'article', 'story',
+ '头条', 'headline', '突发', 'breaking', '快讯',
+ '直播', 'live', '更新', 'update', '最新',
+ '分析', 'analysis', '评论', 'opinion', 'editorial',
+ '专栏', 'column', '博客', 'blog', '专题', 'topic',
+ '财经', 'finance', 'business', '经济', 'economy',
+ '市场', 'market', '股票', 'stock', '基金', 'fund',
+ '体育', 'sports', '娱乐', 'entertainment', 'ent',
+ '科技', 'tech', 'technology', '汽车', 'auto',
+ '生活', 'lifestyle', '健康', 'health',
+ '科学', 'science', '教育', 'education',
+ '政治', 'politics', '国际', 'international', 'world',
+ '国内', 'china', 'local', '地方'
+ ]
+ },
+
+ search: {
+ name: '搜索引擎',
+ icon: '🔍',
+ color: '#607D8B',
+ domains: [
+ 'google.com',
+ 'google.cn',
+ 'google.com.hk',
+ 'google.com.tw',
+ 'google.co.jp',
+ 'google.co.uk',
+ 'google.de',
+ 'google.fr',
+ 'google.es',
+ 'google.it',
+ 'google.com.br',
+ 'google.com.au',
+ 'google.ca',
+ 'google.com.mx',
+ 'google.co.in',
+ 'bing.com',
+ 'bing.com.cn',
+ 'baidu.com',
+ 'www.baidu.com',
+ 'm.baidu.com',
+ 'image.baidu.com',
+ 'video.baidu.com',
+ 'news.baidu.com',
+ 'zhidao.baidu.com',
+ 'tieba.baidu.com',
+ 'baike.baidu.com',
+ 'wenku.baidu.com',
+ 'music.baidu.com',
+ 'map.baidu.com',
+ 'sogou.com',
+ 'www.sogou.com',
+ 'pic.sogou.com',
+ 'video.sogou.com',
+ 'news.sogou.com',
+ 'zhihu.com',
+ 'zhihu.sogou.com',
+ 'weixin.sogou.com',
+ 'so.com',
+ 'www.so.com',
+ 'image.so.com',
+ 'video.so.com',
+ 'news.so.com',
+ 'm.so.com',
+ 'sm.cn',
+ 'www.sm.cn',
+ 'yandex.com',
+ 'yandex.ru',
+ 'duckduckgo.com',
+ 'ecosia.org',
+ 'qwant.com',
+ 'startpage.com',
+ 'searx.me',
+ 'searx.space',
+ 'mojeek.com',
+ 'swisscows.com',
+ 'metager.org',
+ 'gibiru.com',
+ 'ixquick.com',
+ 'yahoo.com',
+ 'search.yahoo.com',
+ 'yahoo.co.jp',
+ 'yahoo.co.uk',
+ 'aol.com',
+ 'search.aol.com',
+ 'ask.com',
+ 'search.ask.com',
+ 'lycos.com',
+ 'excite.com',
+ 'dogpile.com',
+ 'webcrawler.com',
+ 'infospace.com',
+ 'about.com',
+ 'search.about.com'
+ ],
+ pathPatterns: [
+ '/search',
+ '/webhp',
+ '/?q=',
+ '/search?q=',
+ '/s?wd=',
+ '/s?word=',
+ '/s?query=',
+ '/search?query=',
+ '/search?keyword=',
+ '/web',
+ '/images',
+ '/image',
+ '/videos',
+ '/video',
+ '/news',
+ '/maps',
+ '/map',
+ '/shopping',
+ '/scholar',
+ '/books',
+ '/flights',
+ '/finance',
+ '/translate',
+ '/translator',
+ '/dictionary',
+ '/calculator',
+ '/weather',
+ '/news',
+ '/wiki',
+ '/zhidao',
+ '/baike',
+ '/wenku',
+ '/zhihu',
+ '/tieba'
+ ],
+ titleKeywords: [
+ '搜索', 'search', 'query', '查找', '查询',
+ '结果', 'result', '网页', 'web', '网站',
+ '图片', 'images', 'image', '视频', 'video', 'videos',
+ '新闻', 'news', '地图', 'map', 'maps',
+ '购物', 'shopping', '学术', 'scholar',
+ '翻译', 'translate', '词典', 'dictionary',
+ '百科', 'wiki', 'baike', 'zhidao',
+ '文库', 'wenku', '知乎', 'zhihu', '贴吧', 'tieba',
+ '问答', 'question', 'answer', '百度', 'baidu',
+ '谷歌', 'google', '必应', 'bing', '搜狗', 'sogou',
+ '360搜索', 'so.com', '神马', 'sm.cn'
+ ],
+ queryPatterns: [
+ 'q=',
+ 'query=',
+ 'wd=',
+ 'word=',
+ 'keyword=',
+ 'search=',
+ 'key='
+ ]
+ },
+
+ entertainment: {
+ name: '娱乐休闲',
+ icon: '🎮',
+ color: '#9C27B0',
+ domains: [
+ 'steamcommunity.com',
+ 'store.steampowered.com',
+ 'epicgames.com',
+ 'store.epicgames.com',
+ 'gog.com',
+ 'origin.com',
+ 'ea.com',
+ 'ubisoft.com',
+ 'blizzard.com',
+ 'battle.net',
+ 'riotgames.com',
+ 'leagueoflegends.com',
+ 'playvalorant.com',
+ 'dota2.com',
+ 'csgo.com',
+ 'counter-strike.net',
+ 'teamfortress.com',
+ 'halo.xbox.com',
+ 'forzamotorsport.net',
+ 'gears5.com',
+ 'playstation.com',
+ 'store.playstation.com',
+ 'xbox.com',
+ 'microsoft.com/en-us/store/games',
+ 'nintendo.com',
+ 'nintendo.co.jp',
+ 'nintendo.com/switch',
+ 'roblox.com',
+ 'minecraft.net',
+ 'mojang.com',
+ 'fortnite.com',
+ 'apexlegends.com',
+ 'callofduty.com',
+ 'battlefield.com',
+ 'assassinscreed.com',
+ 'far-cry.ubisoft.com',
+ 'watchdogs.ubisoft.com',
+ 'tomclancy.com',
+ 'rainbow6.com',
+ 'thedivisiongame.com',
+ 'ghost-recon.ubisoft.com',
+ 'cyberpunk.net',
+ 'thewitcher.com',
+ 'cdprojektred.com',
+ 'rockstargames.com',
+ 'gtav.com',
+ 'reddeadredemption.com',
+ 'bns.plaync.com',
+ 'lineage.com',
+ 'aion.plaync.com',
+ 'guildwars2.com',
+ 'finalfantasyxiv.com',
+ 'worldofwarcraft.com',
+ 'warcraft.com',
+ 'hearthstone.com',
+ 'starcraft2.com',
+ 'overwatch.com',
+ 'diablo.com',
+ 'wowchina.com',
+ 'game.163.com',
+ 'play.163.com',
+ 'game.qq.com',
+ 'game.weixin.qq.com',
+ 'gamecenter.qq.com',
+ 'pvp.qq.com',
+ 'lol.qq.com',
+ 'game.qq.com',
+ 'dnf.qq.com',
+ 'cf.qq.com',
+ 'qqgame.qq.com',
+ 'qqgame.qq.com',
+ 'igame.qq.com',
+ 'game.163.com',
+ 'play.163.com',
+ 'mc.163.com',
+ 'stzb.163.com',
+ 'yys.163.com',
+ 'xyq.163.com',
+ 'qnmh.163.com',
+ 'hs.blizzard.cn',
+ 'ow.blizzard.cn',
+ 'sc2.blizzard.cn',
+ 'diablo3.blizzard.cn',
+ 'wow.blizzard.cn',
+ 'game.sina.com.cn',
+ 'games.sina.com.cn',
+ 'game.sohu.com',
+ 'game.china.com',
+ '17173.com',
+ 'game.17173.com',
+ '3dmgame.com',
+ 'gamersky.com',
+ 'ali213.net',
+ 'yxdown.com',
+ 'yxbao.com',
+ 'pcgames.com.cn',
+ 'games.qq.com',
+ 'games.sina.com.cn',
+ 'games.sohu.com',
+ 'game.17173.com',
+ '4399.com',
+ '7k7k.com',
+ '2144.com',
+ '3366.com',
+ 'game.com',
+ 'xiaoyouxi.com',
+ 'flashgame.com',
+ 'miniclip.com',
+ 'kongregate.com',
+ 'newgrounds.com',
+ 'armorgames.com',
+ 'poki.com',
+ 'crazygames.com',
+ 'agame.com',
+ 'y8.com',
+ 'friv.com',
+ 'twitch.tv',
+ 'm.twitch.tv',
+ 'youtube.com/gaming',
+ 'gaming.youtube.com',
+ 'mixer.com',
+ 'dlive.tv',
+ 'trovo.live',
+ 'douyu.com',
+ 'm.douyu.com',
+ 'huya.com',
+ 'm.huya.com',
+ 'longzhu.com',
+ 'quanmin.tv',
+ 'panda.tv',
+ 'zhanqi.tv',
+ 'huomao.com',
+ 'inke.cn',
+ 'yy.com',
+ '6.cn',
+ '9158.com',
+ 'fanxing.com',
+ 'kuwo.cn',
+ 'kugou.com',
+ 'qqmusic.com',
+ 'music.163.com',
+ 'xiami.com',
+ 'spotify.com',
+ 'applemusic.com',
+ 'music.apple.com',
+ 'deezer.com',
+ 'tidal.com',
+ 'soundcloud.com',
+ 'bandcamp.com',
+ 'mixcloud.com',
+ 'last.fm',
+ 'genius.com',
+ 'musixmatch.com',
+ 'shazam.com',
+ 'midomi.com',
+ 'zhihu.com',
+ 'bilibili.com',
+ 'acfun.cn',
+ 'dilidili.com',
+ 'dmhy.org',
+ 'btbtt11.com',
+ 'skr.skr1.cc',
+ 'zimuku.org',
+ 'subhd.com',
+ 'assrt.net'
+ ],
+ pathPatterns: [
+ '/game',
+ '/games',
+ '/play',
+ '/store',
+ '/dlc',
+ '/expansion',
+ '/update',
+ '/patch',
+ '/mod',
+ '/mods',
+ '/cheat',
+ '/trainer',
+ '/walkthrough',
+ '/guide',
+ '/tutorial',
+ '/wiki',
+ '/strategy',
+ '/build',
+ '/character',
+ '/class',
+ '/raid',
+ '/dungeon',
+ '/quest',
+ '/mission',
+ '/achievement',
+ '/trophy',
+ '/leaderboard',
+ '/rank',
+ '/ranking',
+ '/competition',
+ '/tournament',
+ '/esport',
+ '/esports',
+ '/stream',
+ '/live',
+ '/video',
+ '/clip',
+ '/highlight',
+ '/replay',
+ '/vod',
+ '/music',
+ '/song',
+ '/songs',
+ '/album',
+ '/albums',
+ '/playlist',
+ '/playlists',
+ '/artist',
+ '/artists',
+ '/podcast',
+ '/radio',
+ '/station',
+ '/anime',
+ '/animation',
+ '/cartoon',
+ '/comic',
+ '/comics',
+ '/manga',
+ '/manhua',
+ '/manhwa',
+ '/novel',
+ '/lightnovel',
+ '/webnovel',
+ '/fanart',
+ '/cosplay',
+ '/doujin',
+ '/acg',
+ '/otaku'
+ ],
+ titleKeywords: [
+ '游戏', 'game', 'games', 'play', '游玩',
+ 'steam', 'epic', 'xbox', 'playstation', 'switch',
+ '网游', 'online', '手游', 'mobile', '单机', 'single',
+ '攻略', 'guide', 'walkthrough', '教程', 'tutorial',
+ '秘籍', 'cheat', '修改器', 'trainer', '存档', 'save',
+ 'mod', '模组', '补丁', 'patch', '更新', 'update',
+ 'dlc', '扩展', 'expansion', '赛季', 'season',
+ '赛事', 'tournament', '电竞', 'esport', 'esports',
+ '直播', 'stream', 'live', '视频', 'video',
+ '集锦', 'highlight', '精彩', 'replay', '回放',
+ '音乐', 'music', '歌曲', 'song', '专辑', 'album',
+ '歌单', 'playlist', '歌手', 'artist', '电台', 'radio',
+ '动漫', 'anime', '动画', 'animation', '漫画', 'comic',
+ '二次元', '2d', 'acg', '宅', 'otaku',
+ '小说', 'novel', '网文', 'webnovel', '轻小说', 'lightnovel'
+ ]
+ }
+ };
+
+ console.log('📑 ContentTypeClassifier initialized with', Object.keys(this.contentTypePatterns).length, 'categories');
+ }
+
+ async initialize() {
+ console.log('✅ ContentTypeClassifier ready');
+ return true;
+ }
+
+ // ========== 自定义模式管理 ==========
+
+ addCustomPattern(contentType, patterns) {
+ if (!this.contentTypePatterns[contentType]) {
+ console.warn(`⚠️ Content type "${contentType}" not found, creating new category`);
+ this.contentTypePatterns[contentType] = {
+ name: contentType,
+ icon: '📁',
+ color: '#607D8B',
+ domains: [],
+ pathPatterns: [],
+ titleKeywords: []
+ };
+ }
+
+ const category = this.contentTypePatterns[contentType];
+
+ if (patterns.domains) {
+ category.domains = [...new Set([...category.domains, ...patterns.domains])];
+ }
+ if (patterns.pathPatterns) {
+ category.pathPatterns = [...new Set([...category.pathPatterns, ...patterns.pathPatterns])];
+ }
+ if (patterns.titleKeywords) {
+ category.titleKeywords = [...new Set([...category.titleKeywords, ...patterns.titleKeywords])];
+ }
+ if (patterns.fileExtensions) {
+ if (!category.fileExtensions) category.fileExtensions = [];
+ category.fileExtensions = [...new Set([...category.fileExtensions, ...patterns.fileExtensions])];
+ }
+ if (patterns.queryPatterns) {
+ if (!category.queryPatterns) category.queryPatterns = [];
+ category.queryPatterns = [...new Set([...category.queryPatterns, ...patterns.queryPatterns])];
+ }
+
+ console.log('➕ Added custom patterns for', contentType);
+ return true;
+ }
+
+ removeCustomPattern(contentType, patterns) {
+ if (!this.contentTypePatterns[contentType]) {
+ console.warn(`⚠️ Content type "${contentType}" not found`);
+ return false;
+ }
+
+ const category = this.contentTypePatterns[contentType];
+
+ if (patterns.domains) {
+ category.domains = category.domains.filter(d => !patterns.domains.includes(d));
+ }
+ if (patterns.pathPatterns) {
+ category.pathPatterns = category.pathPatterns.filter(p => !patterns.pathPatterns.includes(p));
+ }
+ if (patterns.titleKeywords) {
+ category.titleKeywords = category.titleKeywords.filter(k => !patterns.titleKeywords.includes(k));
+ }
+
+ return true;
+ }
+
+ setCustomWeights(weights) {
+ this.customWeights = {
+ ...this.defaultWeights,
+ ...weights
+ };
+ console.log('⚖️ Custom weights set:', this.customWeights);
+ }
+
+ // ========== 分类核心方法 ==========
+
+ classify(tab) {
+ const url = tab.url || '';
+ const title = tab.title || '';
+
+ const scores = new Map();
+ const matches = [];
+
+ for (const [type, config] of Object.entries(this.contentTypePatterns)) {
+ const score = this.calculateMatchScore(url, title, config);
+ if (score > 0) {
+ scores.set(type, score);
+ matches.push({
+ type,
+ score,
+ name: config.name,
+ icon: config.icon,
+ color: config.color
+ });
+ }
+ }
+
+ matches.sort((a, b) => b.score - a.score);
+
+ if (matches.length === 0) {
+ return {
+ type: 'other',
+ name: '其他',
+ icon: '🌐',
+ color: '#607D8B',
+ score: 0,
+ matches: []
+ };
+ }
+
+ return {
+ ...matches[0],
+ matches: matches.slice(0, 5)
+ };
+ }
+
+ calculateMatchScore(url, title, config) {
+ let totalScore = 0;
+ const weights = this.customWeights || this.defaultWeights;
+
+ const domainScore = this.matchDomain(url, config.domains || []);
+ totalScore += domainScore * weights.domain;
+
+ const pathScore = this.matchPath(url, config.pathPatterns || []);
+ totalScore += pathScore * weights.path;
+
+ const titleScore = this.matchTitle(title, config.titleKeywords || []);
+ totalScore += titleScore * weights.title;
+
+ const queryScore = this.matchQuery(url, config.queryPatterns || []);
+ totalScore += queryScore * weights.query;
+
+ const fileExtScore = this.matchFileExtension(url, config.fileExtensions || []);
+ totalScore += fileExtScore * 0.15;
+
+ return Math.min(totalScore, 1);
+ }
+
+ matchDomain(url, domains) {
+ if (!url || domains.length === 0) return 0;
+
+ try {
+ const urlObj = new URL(url);
+ const hostname = urlObj.hostname.toLowerCase();
+
+ for (const domain of domains) {
+ const domainLower = domain.toLowerCase();
+
+ if (hostname === domainLower || hostname.endsWith(`.${domainLower}`)) {
+ return 1;
+ }
+ }
+ } catch (e) {
+ const urlLower = url.toLowerCase();
+ for (const domain of domains) {
+ const domainLower = domain.toLowerCase();
+ if (urlLower.includes(domainLower)) {
+ return 0.8;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ matchPath(url, patterns) {
+ if (!url || patterns.length === 0) return 0;
+
+ try {
+ const urlObj = new URL(url);
+ const pathname = urlObj.pathname.toLowerCase();
+
+ for (const pattern of patterns) {
+ const patternLower = pattern.toLowerCase();
+
+ if (pathname.includes(patternLower) || pathname.startsWith(patternLower)) {
+ return 1;
+ }
+ }
+ } catch (e) {
+ const urlLower = url.toLowerCase();
+ for (const pattern of patterns) {
+ const patternLower = pattern.toLowerCase();
+ if (urlLower.includes(patternLower)) {
+ return 0.7;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ matchTitle(title, keywords) {
+ if (!title || keywords.length === 0) return 0;
+
+ const titleLower = title.toLowerCase();
+ let matchCount = 0;
+
+ for (const keyword of keywords) {
+ const keywordLower = keyword.toLowerCase();
+ if (titleLower.includes(keywordLower)) {
+ matchCount++;
+ }
+ }
+
+ if (matchCount === 0) return 0;
+ return Math.min(matchCount / Math.max(1, Math.floor(keywords.length * 0.3)), 1);
+ }
+
+ matchQuery(url, patterns) {
+ if (!url || patterns.length === 0) return 0;
+
+ try {
+ const urlObj = new URL(url);
+ const searchParams = urlObj.searchParams;
+
+ for (const pattern of patterns) {
+ const paramName = pattern.replace('=', '').toLowerCase();
+ if (searchParams.has(paramName)) {
+ return 1;
+ }
+ }
+ } catch (e) {
+ const urlLower = url.toLowerCase();
+ for (const pattern of patterns) {
+ if (urlLower.includes(pattern.toLowerCase())) {
+ return 0.5;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ matchFileExtension(url, extensions) {
+ if (!url || extensions.length === 0) return 0;
+
+ const urlLower = url.toLowerCase();
+
+ for (const ext of extensions) {
+ const extLower = ext.toLowerCase();
+ if (urlLower.endsWith(extLower) || urlLower.includes(`${extLower}?`) || urlLower.includes(`${extLower}#`)) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ // ========== 批量分类 ==========
+
+ classifyAll(tabs) {
+ const groups = new Map();
+ const ungrouped = [];
+
+ for (const tab of tabs) {
+ const result = this.classify(tab);
+
+ if (result.score > 0.3) {
+ const key = result.type;
+ if (!groups.has(key)) {
+ groups.set(key, {
+ id: `content_${key}`,
+ name: result.name,
+ type: 'content-type',
+ contentType: key,
+ icon: result.icon,
+ color: result.color,
+ tabs: [],
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+ groups.get(key).tabs.push(tab);
+ } else {
+ ungrouped.push(tab);
+ }
+ }
+
+ const resultGroups = Array.from(groups.values())
+ .sort((a, b) => b.tabs.length - a.tabs.length);
+
+ if (ungrouped.length > 0) {
+ resultGroups.push({
+ id: 'content_other',
+ name: '其他',
+ type: 'content-type',
+ contentType: 'other',
+ icon: '🌐',
+ color: '#607D8B',
+ tabs: ungrouped,
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+
+ return resultGroups;
+ }
+
+ // ========== 获取分类信息 ==========
+
+ getCategories() {
+ const categories = [];
+ for (const [type, config] of Object.entries(this.contentTypePatterns)) {
+ categories.push({
+ type,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ domainCount: (config.domains || []).length,
+ patternCount: (config.pathPatterns || []).length + (config.titleKeywords || []).length
+ });
+ }
+ return categories;
+ }
+
+ getCategoryInfo(type) {
+ const config = this.contentTypePatterns[type];
+ if (!config) return null;
+
+ return {
+ type,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ domains: config.domains || [],
+ pathPatterns: config.pathPatterns || [],
+ titleKeywords: config.titleKeywords || [],
+ fileExtensions: config.fileExtensions || [],
+ queryPatterns: config.queryPatterns || []
+ };
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = ContentTypeClassifier;
+}
+
+if (typeof window !== 'undefined') {
+ window.ContentTypeClassifier = ContentTypeClassifier;
+}
diff --git a/utils/group-cache-manager.js b/utils/group-cache-manager.js
new file mode 100644
index 0000000..1c55f40
--- /dev/null
+++ b/utils/group-cache-manager.js
@@ -0,0 +1,646 @@
+/**
+ * GroupCacheManager - 分组缓存管理器
+ *
+ * 实现功能:
+ * - 分组结果实时更新与增量同步
+ * - 避免页面切换时分组错乱
+ * - 缓存预热和失效策略
+ * - 变更追踪和差异计算
+ */
+
+class GroupCacheManager {
+ constructor(storageManager, eventBus, options = {}) {
+ this.storageManager = storageManager;
+ this.eventBus = eventBus;
+
+ this.cache = {
+ groups: new Map(),
+ tabs: new Map(),
+ groupToTabs: new Map(),
+ tabToGroup: new Map(),
+ metadata: {
+ lastUpdated: 0,
+ version: 0,
+ hash: ''
+ }
+ };
+
+ this.pendingUpdates = [];
+ this.isSyncing = false;
+ this.dirtyFlags = new Set();
+
+ this.cacheTimeout = options.cacheTimeout || 5 * 60 * 1000;
+ this.maxHistorySize = options.maxHistorySize || 100;
+ this.changeHistory = [];
+
+ this.listeners = {
+ groupUpdated: [],
+ tabMoved: [],
+ cacheInvalidated: [],
+ syncCompleted: []
+ };
+
+ this.initialized = false;
+
+ console.log('💾 GroupCacheManager initialized');
+ }
+
+ async initialize() {
+ if (this.initialized) return true;
+
+ await this.loadFromStorage();
+ this.setupEventListeners();
+
+ this.initialized = true;
+ console.log('✅ GroupCacheManager initialized');
+ return true;
+ }
+
+ setupEventListeners() {
+ if (!this.eventBus) return;
+
+ this.eventBus.on('tab_created', (tabData) => {
+ this.handleTabCreated(tabData);
+ });
+
+ this.eventBus.on('tab_removed', ({ tabId, tabData }) => {
+ this.handleTabRemoved(tabData);
+ });
+
+ this.eventBus.on('tab_updated', (tabData) => {
+ this.handleTabUpdated(tabData);
+ });
+
+ this.eventBus.on('tab_moved_to_group', ({ tab, groupId }) => {
+ this.handleTabMoved(tab, groupId);
+ });
+
+ this.eventBus.on('group_created', (group) => {
+ this.handleGroupCreated(group);
+ });
+
+ this.eventBus.on('group_updated', ({ group }) => {
+ this.handleGroupUpdated(group);
+ });
+
+ this.eventBus.on('group_deleted', ({ groupId, tabIds }) => {
+ this.handleGroupDeleted(groupId);
+ });
+
+ this.eventBus.on('tab_group_assigned', ({ tabData, groupId }) => {
+ this.handleTabGroupAssigned(tabData, groupId);
+ });
+ }
+
+ // ========== 缓存核心操作 ==========
+
+ async getGroup(groupId) {
+ await this.ensureCacheFresh();
+
+ const group = this.cache.groups.get(groupId);
+ if (!group) return null;
+
+ const tabIds = this.cache.groupToTabs.get(groupId) || [];
+ const tabs = tabIds
+ .map(id => this.cache.tabs.get(id))
+ .filter(Boolean);
+
+ return {
+ ...group,
+ tabs
+ };
+ }
+
+ async getAllGroups() {
+ await this.ensureCacheFresh();
+
+ const groups = [];
+ for (const [groupId, group] of this.cache.groups.entries()) {
+ const tabIds = this.cache.groupToTabs.get(groupId) || [];
+ const tabs = tabIds
+ .map(id => this.cache.tabs.get(id))
+ .filter(Boolean);
+
+ groups.push({
+ ...group,
+ tabs
+ });
+ }
+
+ return groups.sort((a, b) => (b.priority || 0) - (a.priority || 0));
+ }
+
+ getTab(tabId) {
+ return this.cache.tabs.get(tabId) || null;
+ }
+
+ getTabGroup(tabId) {
+ const groupId = this.cache.tabToGroup.get(tabId);
+ if (!groupId) return null;
+ return this.cache.groups.get(groupId) || null;
+ }
+
+ // ========== 增量更新处理 ==========
+
+ handleTabCreated(tabData) {
+ const tabId = tabData.id;
+
+ this.cache.tabs.set(tabId, { ...tabData });
+
+ const groupId = tabData.groupId || 'ungrouped';
+ this.cache.tabToGroup.set(tabId, groupId);
+
+ if (!this.cache.groupToTabs.has(groupId)) {
+ this.cache.groupToTabs.set(groupId, []);
+ }
+ const tabIds = this.cache.groupToTabs.get(groupId);
+ if (!tabIds.includes(tabId)) {
+ tabIds.push(tabId);
+ }
+
+ this.markDirty('tabs', tabId);
+ this.recordChange({
+ type: 'tab_created',
+ tabId,
+ groupId,
+ timestamp: Date.now()
+ });
+
+ this.notify('groupUpdated', { groupId, tabId, action: 'add' });
+ }
+
+ handleTabRemoved(tabData) {
+ if (!tabData) return;
+
+ const tabId = tabData.id;
+ const groupId = this.cache.tabToGroup.get(tabId);
+
+ this.cache.tabs.delete(tabId);
+ this.cache.tabToGroup.delete(tabId);
+
+ if (groupId) {
+ const tabIds = this.cache.groupToTabs.get(groupId);
+ if (tabIds) {
+ const index = tabIds.indexOf(tabId);
+ if (index > -1) {
+ tabIds.splice(index, 1);
+ }
+ }
+ }
+
+ this.markDirty('tabs', tabId);
+ this.recordChange({
+ type: 'tab_removed',
+ tabId,
+ groupId,
+ timestamp: Date.now()
+ });
+
+ if (groupId) {
+ this.notify('groupUpdated', { groupId, tabId, action: 'remove' });
+ }
+ }
+
+ handleTabUpdated(tabData) {
+ const tabId = tabData.id;
+ const existingTab = this.cache.tabs.get(tabId);
+
+ if (existingTab) {
+ const oldGroupId = existingTab.groupId;
+ const newGroupId = tabData.groupId || 'ungrouped';
+
+ if (oldGroupId !== newGroupId) {
+ this.handleTabMoved(tabData, newGroupId);
+ }
+
+ this.cache.tabs.set(tabId, { ...existingTab, ...tabData });
+ } else {
+ this.handleTabCreated(tabData);
+ }
+
+ this.markDirty('tabs', tabId);
+ }
+
+ handleTabMoved(tab, newGroupId) {
+ const tabId = tab.id;
+ const oldGroupId = this.cache.tabToGroup.get(tabId);
+
+ if (oldGroupId && oldGroupId !== newGroupId) {
+ const oldTabIds = this.cache.groupToTabs.get(oldGroupId);
+ if (oldTabIds) {
+ const index = oldTabIds.indexOf(tabId);
+ if (index > -1) {
+ oldTabIds.splice(index, 1);
+ }
+ }
+ this.notify('groupUpdated', { groupId: oldGroupId, tabId, action: 'remove' });
+ }
+
+ this.cache.tabToGroup.set(tabId, newGroupId);
+
+ if (!this.cache.groupToTabs.has(newGroupId)) {
+ this.cache.groupToTabs.set(newGroupId, []);
+ }
+ const newTabIds = this.cache.groupToTabs.get(newGroupId);
+ if (!newTabIds.includes(tabId)) {
+ newTabIds.push(tabId);
+ }
+
+ this.markDirty('tabToGroup', tabId);
+ this.recordChange({
+ type: 'tab_moved',
+ tabId,
+ oldGroupId,
+ newGroupId,
+ timestamp: Date.now()
+ });
+
+ this.notify('tabMoved', { tabId, oldGroupId, newGroupId });
+ this.notify('groupUpdated', { groupId: newGroupId, tabId, action: 'add' });
+ }
+
+ handleTabGroupAssigned(tabData, groupId) {
+ this.handleTabMoved(tabData, groupId);
+ }
+
+ handleGroupCreated(group) {
+ const groupId = group.id;
+
+ this.cache.groups.set(groupId, { ...group });
+
+ if (!this.cache.groupToTabs.has(groupId)) {
+ this.cache.groupToTabs.set(groupId, []);
+ }
+
+ if (group.tabs && Array.isArray(group.tabs)) {
+ for (const tab of group.tabs) {
+ const tabId = tab.id;
+ this.cache.tabToGroup.set(tabId, groupId);
+
+ const tabIds = this.cache.groupToTabs.get(groupId);
+ if (!tabIds.includes(tabId)) {
+ tabIds.push(tabId);
+ }
+ }
+ }
+
+ this.markDirty('groups', groupId);
+ this.recordChange({
+ type: 'group_created',
+ groupId,
+ timestamp: Date.now()
+ });
+
+ this.notify('groupUpdated', { groupId, action: 'create' });
+ }
+
+ handleGroupUpdated(group) {
+ const existingGroup = this.cache.groups.get(group.id);
+ if (existingGroup) {
+ this.cache.groups.set(group.id, { ...existingGroup, ...group });
+ } else {
+ this.cache.groups.set(group.id, { ...group });
+ }
+
+ this.markDirty('groups', group.id);
+ this.recordChange({
+ type: 'group_updated',
+ groupId: group.id,
+ timestamp: Date.now()
+ });
+
+ this.notify('groupUpdated', { groupId: group.id, action: 'update' });
+ }
+
+ handleGroupDeleted(groupId) {
+ const tabIds = this.cache.groupToTabs.get(groupId) || [];
+
+ for (const tabId of tabIds) {
+ this.cache.tabToGroup.delete(tabId);
+ }
+
+ this.cache.groups.delete(groupId);
+ this.cache.groupToTabs.delete(groupId);
+
+ this.markDirty('groups', groupId);
+ this.recordChange({
+ type: 'group_deleted',
+ groupId,
+ tabIds,
+ timestamp: Date.now()
+ });
+
+ this.notify('groupUpdated', { groupId, action: 'delete' });
+ }
+
+ // ========== 脏标记和同步 ==========
+
+ markDirty(type, id) {
+ this.dirtyFlags.add(`${type}:${id}`);
+ this.cache.metadata.lastUpdated = Date.now();
+ this.cache.metadata.version++;
+
+ this.scheduleSync();
+ }
+
+ scheduleSync() {
+ if (this.syncTimeout) {
+ clearTimeout(this.syncTimeout);
+ }
+
+ this.syncTimeout = setTimeout(() => {
+ this.performSync();
+ }, 100);
+ }
+
+ async performSync() {
+ if (this.isSyncing || this.dirtyFlags.size === 0) {
+ return;
+ }
+
+ this.isSyncing = true;
+
+ try {
+ await this.saveToStorage();
+ this.dirtyFlags.clear();
+
+ this.notify('syncCompleted', {
+ timestamp: Date.now(),
+ version: this.cache.metadata.version
+ });
+ } catch (error) {
+ console.error('❌ Failed to sync cache:', error);
+ } finally {
+ this.isSyncing = false;
+ }
+ }
+
+ async ensureCacheFresh() {
+ const now = Date.now();
+ const age = now - this.cache.metadata.lastUpdated;
+
+ if (age > this.cacheTimeout && !this.isSyncing) {
+ console.log('🔄 Cache expired, refreshing...');
+ await this.loadFromStorage();
+ }
+ }
+
+ // ========== 持久化 ==========
+
+ async saveToStorage() {
+ const cacheData = {
+ groups: Object.fromEntries(this.cache.groups),
+ tabs: Object.fromEntries(this.cache.tabs),
+ groupToTabs: Object.fromEntries(
+ Array.from(this.cache.groupToTabs.entries()).map(([k, v]) => [k, v])
+ ),
+ tabToGroup: Object.fromEntries(this.cache.tabToGroup),
+ metadata: {
+ ...this.cache.metadata,
+ hash: this.generateHash()
+ }
+ };
+
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ 'tabflow:group_cache': cacheData }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+
+ console.log('💾 Cache saved, version:', this.cache.metadata.version);
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to save cache:', error);
+ return false;
+ }
+ }
+
+ async loadFromStorage() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get('tabflow:group_cache', (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ const cacheData = result['tabflow:group_cache'];
+
+ if (cacheData) {
+ const currentHash = this.generateHash();
+ if (cacheData.metadata && cacheData.metadata.hash === currentHash) {
+ console.log('ℹ️ Cache unchanged, skipping load');
+ return false;
+ }
+
+ this.cache.groups = new Map(Object.entries(cacheData.groups || {}));
+ this.cache.tabs = new Map(Object.entries(cacheData.tabs || {}));
+ this.cache.groupToTabs = new Map(
+ Object.entries(cacheData.groupToTabs || {}).map(([k, v]) => [k, Array.isArray(v) ? v : []])
+ );
+ this.cache.tabToGroup = new Map(Object.entries(cacheData.tabToGroup || {}));
+ this.cache.metadata = cacheData.metadata || {
+ lastUpdated: Date.now(),
+ version: 0,
+ hash: ''
+ };
+
+ console.log('📥 Cache loaded, version:', this.cache.metadata.version);
+ this.notify('cacheInvalidated', { reason: 'loaded' });
+ return true;
+ }
+
+ return false;
+ } catch (error) {
+ console.warn('⚠️ Failed to load cache:', error);
+ return false;
+ }
+ }
+
+ generateHash() {
+ let hash = 0;
+ const str = `${this.cache.groups.size}-${this.cache.tabs.size}-${this.cache.metadata.version}`;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash;
+ }
+ return hash.toString(36);
+ }
+
+ // ========== 变更历史 ==========
+
+ recordChange(change) {
+ this.changeHistory.push(change);
+
+ if (this.changeHistory.length > this.maxHistorySize) {
+ this.changeHistory.shift();
+ }
+ }
+
+ getChangeHistory(since = 0) {
+ if (since === 0) {
+ return [...this.changeHistory];
+ }
+ return this.changeHistory.filter(c => c.timestamp >= since);
+ }
+
+ clearHistory() {
+ this.changeHistory = [];
+ }
+
+ // ========== 监听系统 ==========
+
+ on(event, callback) {
+ if (this.listeners[event]) {
+ this.listeners[event].push(callback);
+ }
+ }
+
+ off(event, callback) {
+ if (this.listeners[event]) {
+ const index = this.listeners[event].indexOf(callback);
+ if (index > -1) {
+ this.listeners[event].splice(index, 1);
+ }
+ }
+ }
+
+ notify(event, data) {
+ if (this.listeners[event]) {
+ for (const callback of this.listeners[event]) {
+ try {
+ callback(data);
+ } catch (error) {
+ console.error(`❌ Error in ${event} listener:`, error);
+ }
+ }
+ }
+ }
+
+ // ========== 缓存操作 ==========
+
+ invalidateCache(reason = 'manual') {
+ this.cache.groups.clear();
+ this.cache.tabs.clear();
+ this.cache.groupToTabs.clear();
+ this.cache.tabToGroup.clear();
+ this.cache.metadata = {
+ lastUpdated: 0,
+ version: 0,
+ hash: ''
+ };
+ this.dirtyFlags.clear();
+
+ console.log('🗑️ Cache invalidated, reason:', reason);
+ this.notify('cacheInvalidated', { reason });
+ }
+
+ preloadFromData(tabs, groups) {
+ this.invalidateCache('preload');
+
+ for (const group of groups) {
+ this.cache.groups.set(group.id, { ...group });
+ this.cache.groupToTabs.set(group.id, []);
+ }
+
+ for (const tab of tabs) {
+ const tabId = tab.id;
+ this.cache.tabs.set(tabId, { ...tab });
+
+ const groupId = tab.groupId || 'ungrouped';
+ this.cache.tabToGroup.set(tabId, groupId);
+
+ if (!this.cache.groupToTabs.has(groupId)) {
+ this.cache.groupToTabs.set(groupId, []);
+ }
+ const tabIds = this.cache.groupToTabs.get(groupId);
+ if (!tabIds.includes(tabId)) {
+ tabIds.push(tabId);
+ }
+ }
+
+ this.cache.metadata.lastUpdated = Date.now();
+ this.cache.metadata.version++;
+
+ console.log('📦 Cache preloaded with', tabs.length, 'tabs and', groups.length, 'groups');
+ this.notify('cacheInvalidated', { reason: 'preloaded' });
+ }
+
+ // ========== 差异计算 ==========
+
+ calculateDifferences(oldState, newState) {
+ const differences = {
+ addedGroups: [],
+ removedGroups: [],
+ updatedGroups: [],
+ addedTabs: [],
+ removedTabs: [],
+ movedTabs: [],
+ updatedTabs: []
+ };
+
+ const oldGroups = oldState.groups || new Map();
+ const newGroups = newState.groups || new Map();
+
+ for (const [groupId, group] of newGroups.entries()) {
+ if (!oldGroups.has(groupId)) {
+ differences.addedGroups.push({ groupId, group });
+ } else {
+ const oldGroup = oldGroups.get(groupId);
+ if (JSON.stringify(oldGroup) !== JSON.stringify(group)) {
+ differences.updatedGroups.push({ groupId, oldGroup, newGroup: group });
+ }
+ }
+ }
+
+ for (const [groupId] of oldGroups.entries()) {
+ if (!newGroups.has(groupId)) {
+ differences.removedGroups.push({ groupId });
+ }
+ }
+
+ return differences;
+ }
+
+ getCurrentState() {
+ return {
+ groups: new Map(this.cache.groups),
+ tabs: new Map(this.cache.tabs),
+ groupToTabs: new Map(this.cache.groupToTabs),
+ tabToGroup: new Map(this.cache.tabToGroup),
+ metadata: { ...this.cache.metadata }
+ };
+ }
+
+ // ========== 统计信息 ==========
+
+ getStats() {
+ return {
+ groupCount: this.cache.groups.size,
+ tabCount: this.cache.tabs.size,
+ lastUpdated: this.cache.metadata.lastUpdated,
+ version: this.cache.metadata.version,
+ dirtyFlagCount: this.dirtyFlags.size,
+ isSyncing: this.isSyncing,
+ changeHistorySize: this.changeHistory.length,
+ cacheAge: Date.now() - this.cache.metadata.lastUpdated
+ };
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = GroupCacheManager;
+}
+
+if (typeof window !== 'undefined') {
+ window.GroupCacheManager = GroupCacheManager;
+}
diff --git a/utils/ml-classifier.js b/utils/ml-classifier.js
new file mode 100644
index 0000000..7cafe36
--- /dev/null
+++ b/utils/ml-classifier.js
@@ -0,0 +1,777 @@
+/**
+ * MLClassifier - 轻量化机器学习分类器
+ *
+ * 实现Naive Bayes分类器、逻辑回归、决策树等轻量化算法
+ * 用于智能标签页分组、场景分类、内容类型识别
+ */
+
+class MLClassifier {
+ constructor(options = {}) {
+ this.algorithm = options.algorithm || options.modelType || 'naive-bayes';
+ this.modelType = this.algorithm;
+ this.storageKey = options.storageKey || 'tabflow:ml_model';
+ this.vocabulary = new Map();
+ this.classCounts = new Map();
+ this.classWordCounts = new Map();
+ this.weights = new Map();
+ this.trained = false;
+ this.isTrained = false;
+ this.hasModel = false;
+ this.trainingData = [];
+ this.maxTrainingSamples = options.maxTrainingSamples || 1000;
+ this.learningRate = options.learningRate || 0.01;
+ this.regularization = options.regularization || 0.001;
+ this.enableOnlineLearning = options.enableOnlineLearning !== false;
+ this.initialized = false;
+
+ console.log('🤖 MLClassifier initialized with model:', this.modelType);
+ }
+
+ async initialize() {
+ if (this.initialized) {
+ return true;
+ }
+
+ try {
+ await this.load();
+ this.initialized = true;
+ console.log('✅ MLClassifier fully initialized');
+ return true;
+ } catch (error) {
+ console.warn('⚠️ MLClassifier initialization with saved model failed, using fresh model:', error);
+ this.initialized = true;
+ return true;
+ }
+ }
+
+ // ========== 文本预处理 ==========
+
+ tokenize(text) {
+ if (!text || typeof text !== 'string') {
+ return [];
+ }
+
+ const lowerText = text.toLowerCase();
+
+ const tokens = lowerText
+ .replace(/[^\w\u4e00-\u9fa5\s]/g, ' ')
+ .split(/\s+/)
+ .filter(token => token.length > 1);
+
+ const ngrams = [];
+ for (const token of tokens) {
+ ngrams.push(token);
+ }
+
+ for (let i = 0; i < tokens.length - 1; i++) {
+ ngrams.push(`${tokens[i]}_${tokens[i + 1]}`);
+ }
+
+ return ngrams;
+ }
+
+ extractFeatures(tab) {
+ const features = {
+ tokens: [],
+ domain: '',
+ domainParts: [],
+ path: '',
+ pathParts: [],
+ titleTokens: [],
+ urlTokens: [],
+ metadata: {
+ visitCount: tab.visitCount || 1,
+ lastAccessed: tab.lastAccessed || Date.now(),
+ openedAt: tab.openedAt || Date.now()
+ }
+ };
+
+ const url = tab.url || '';
+ const title = tab.title || '';
+
+ try {
+ const urlObj = new URL(url);
+ features.domain = urlObj.hostname;
+ features.domainParts = urlObj.hostname.split('.');
+ features.path = urlObj.pathname;
+ features.pathParts = urlObj.pathname.split('/').filter(p => p);
+
+ const queryParams = [];
+ urlObj.searchParams.forEach((value, key) => {
+ queryParams.push(key);
+ });
+ features.queryParams = queryParams;
+ } catch (e) {
+ features.domain = url;
+ }
+
+ features.titleTokens = this.tokenize(title);
+ features.urlTokens = this.tokenize(url);
+ features.domainTokens = this.tokenize(features.domain);
+
+ features.tokens = [
+ ...features.titleTokens,
+ ...features.urlTokens,
+ ...features.domainTokens
+ ];
+
+ return features;
+ }
+
+ // ========== Naive Bayes 分类器 ==========
+
+ trainNaiveBayes(trainingData) {
+ this.vocabulary.clear();
+ this.classCounts.clear();
+ this.classWordCounts.clear();
+
+ for (const sample of trainingData) {
+ const { features, label } = sample;
+
+ if (!this.classCounts.has(label)) {
+ this.classCounts.set(label, 0);
+ this.classWordCounts.set(label, new Map());
+ }
+
+ this.classCounts.set(label, (this.classCounts.get(label) || 0) + 1);
+
+ const wordCounts = this.classWordCounts.get(label);
+ for (const token of features.tokens) {
+ this.vocabulary.set(token, (this.vocabulary.get(token) || 0) + 1);
+ wordCounts.set(token, (wordCounts.get(token) || 0) + 1);
+ }
+ }
+
+ this.trained = true;
+ this.isTrained = true;
+ this.hasModel = this.classCounts.size > 0;
+ console.log('✅ Naive Bayes trained with', trainingData.length, 'samples');
+ }
+
+ predictNaiveBayes(features) {
+ if (!this.trained) {
+ return null;
+ }
+
+ const totalSamples = Array.from(this.classCounts.values()).reduce((a, b) => a + b, 0);
+ const vocabSize = this.vocabulary.size;
+
+ let bestLabel = null;
+ let bestScore = -Infinity;
+
+ for (const [label, count] of this.classCounts.entries()) {
+ const prior = Math.log(count / totalSamples);
+
+ let likelihood = 0;
+ const wordCounts = this.classWordCounts.get(label);
+ const totalWords = Array.from(wordCounts.values()).reduce((a, b) => a + b, 0);
+
+ for (const token of features.tokens) {
+ const wordCount = wordCounts.get(token) || 0;
+ likelihood += Math.log((wordCount + 1) / (totalWords + vocabSize));
+ }
+
+ const score = prior + likelihood;
+
+ if (score > bestScore) {
+ bestScore = score;
+ bestLabel = label;
+ }
+ }
+
+ return { label: bestLabel, score: bestScore };
+ }
+
+ // ========== 逻辑回归分类器 ==========
+
+ sigmoid(z) {
+ if (z >= 500) return 1;
+ if (z <= -500) return 0;
+ return 1 / (1 + Math.exp(-z));
+ }
+
+ trainLogisticRegression(trainingData, iterations = 100) {
+ const allFeatures = new Set();
+ for (const sample of trainingData) {
+ for (const token of sample.features.tokens) {
+ allFeatures.add(token);
+ }
+ }
+
+ const featureIndex = new Map();
+ let idx = 0;
+ for (const feature of allFeatures) {
+ featureIndex.set(feature, idx++);
+ }
+ featureIndex.set('BIAS', idx);
+
+ const numFeatures = featureIndex.size;
+ const weights = new Float64Array(numFeatures);
+
+ const labels = new Set(trainingData.map(d => d.label));
+ const isBinary = labels.size === 2;
+ const labelList = Array.from(labels);
+
+ for (let iter = 0; iter < iterations; iter++) {
+ for (const sample of trainingData) {
+ const features = sample.features.tokens;
+ const label = sample.label;
+
+ let z = weights[featureIndex.get('BIAS')];
+ for (const token of features) {
+ if (featureIndex.has(token)) {
+ z += weights[featureIndex.get(token)];
+ }
+ }
+
+ const prediction = this.sigmoid(z);
+
+ let target;
+ if (isBinary) {
+ target = label === labelList[0] ? 1 : 0;
+ } else {
+ target = 1;
+ }
+
+ const error = target - prediction;
+
+ weights[featureIndex.get('BIAS')] += this.learningRate * error;
+ for (const token of features) {
+ if (featureIndex.has(token)) {
+ const weightIdx = featureIndex.get(token);
+ weights[weightIdx] += this.learningRate * error - this.regularization * weights[weightIdx];
+ }
+ }
+ }
+ }
+
+ this.weights.clear();
+ for (const [feature, index] of featureIndex.entries()) {
+ this.weights.set(feature, weights[index]);
+ }
+ this.featureIndex = featureIndex;
+ this.labels = labelList;
+ this.trained = true;
+ this.isTrained = true;
+ this.hasModel = true;
+
+ console.log('✅ Logistic Regression trained with', trainingData.length, 'samples');
+ }
+
+ predictLogisticRegression(features) {
+ if (!this.trained || !this.featureIndex) {
+ return null;
+ }
+
+ let z = this.weights.get('BIAS') || 0;
+ for (const token of features.tokens) {
+ if (this.weights.has(token)) {
+ z += this.weights.get(token);
+ }
+ }
+
+ const probability = this.sigmoid(z);
+
+ return {
+ label: probability > 0.5 ? (this.labels?.[0] || 'positive') : (this.labels?.[1] || 'negative'),
+ score: probability,
+ probability
+ };
+ }
+
+ // ========== 决策树分类器 (简化版) ==========
+
+ trainDecisionTree(trainingData, maxDepth = 5) {
+ this.decisionTree = this.buildTree(trainingData, 0, maxDepth);
+ this.trained = true;
+ this.isTrained = true;
+ this.hasModel = true;
+ console.log('✅ Decision Tree trained with', trainingData.length, 'samples');
+ }
+
+ buildTree(data, depth, maxDepth) {
+ if (depth >= maxDepth || data.length < 2) {
+ const labelCounts = new Map();
+ for (const sample of data) {
+ labelCounts.set(sample.label, (labelCounts.get(sample.label) || 0) + 1);
+ }
+ let maxCount = 0;
+ let majorityLabel = null;
+ for (const [label, count] of labelCounts.entries()) {
+ if (count > maxCount) {
+ maxCount = count;
+ majorityLabel = label;
+ }
+ }
+ return { type: 'leaf', label: majorityLabel, count: data.length };
+ }
+
+ const bestSplit = this.findBestSplit(data);
+
+ if (!bestSplit) {
+ const labelCounts = new Map();
+ for (const sample of data) {
+ labelCounts.set(sample.label, (labelCounts.get(sample.label) || 0) + 1);
+ }
+ let maxCount = 0;
+ let majorityLabel = null;
+ for (const [label, count] of labelCounts.entries()) {
+ if (count > maxCount) {
+ maxCount = count;
+ majorityLabel = label;
+ }
+ }
+ return { type: 'leaf', label: majorityLabel, count: data.length };
+ }
+
+ const leftData = [];
+ const rightData = [];
+
+ for (const sample of data) {
+ if (sample.features.tokens.includes(bestSplit.feature)) {
+ leftData.push(sample);
+ } else {
+ rightData.push(sample);
+ }
+ }
+
+ return {
+ type: 'node',
+ feature: bestSplit.feature,
+ informationGain: bestSplit.gain,
+ left: this.buildTree(leftData, depth + 1, maxDepth),
+ right: this.buildTree(rightData, depth + 1, maxDepth)
+ };
+ }
+
+ findBestSplit(data) {
+ const allFeatures = new Set();
+ for (const sample of data) {
+ for (const token of sample.features.tokens) {
+ allFeatures.add(token);
+ }
+ }
+
+ let bestFeature = null;
+ let bestGain = 0;
+
+ const baseEntropy = this.calculateEntropy(data);
+
+ for (const feature of allFeatures) {
+ const hasFeature = [];
+ const noFeature = [];
+
+ for (const sample of data) {
+ if (sample.features.tokens.includes(feature)) {
+ hasFeature.push(sample);
+ } else {
+ noFeature.push(sample);
+ }
+ }
+
+ if (hasFeature.length === 0 || noFeature.length === 0) continue;
+
+ const hasEntropy = this.calculateEntropy(hasFeature);
+ const noEntropy = this.calculateEntropy(noFeature);
+
+ const weightedEntropy =
+ (hasFeature.length / data.length) * hasEntropy +
+ (noFeature.length / data.length) * noEntropy;
+
+ const gain = baseEntropy - weightedEntropy;
+
+ if (gain > bestGain) {
+ bestGain = gain;
+ bestFeature = feature;
+ }
+ }
+
+ return bestFeature ? { feature: bestFeature, gain: bestGain } : null;
+ }
+
+ calculateEntropy(data) {
+ const labelCounts = new Map();
+ for (const sample of data) {
+ labelCounts.set(sample.label, (labelCounts.get(sample.label) || 0) + 1);
+ }
+
+ let entropy = 0;
+ const total = data.length;
+
+ for (const count of labelCounts.values()) {
+ const probability = count / total;
+ entropy -= probability * Math.log2(probability);
+ }
+
+ return entropy;
+ }
+
+ predictDecisionTree(features) {
+ if (!this.trained || !this.decisionTree) {
+ return null;
+ }
+
+ let node = this.decisionTree;
+
+ while (node.type !== 'leaf') {
+ if (features.tokens.includes(node.feature)) {
+ node = node.left;
+ } else {
+ node = node.right;
+ }
+ }
+
+ return { label: node.label, count: node.count };
+ }
+
+ // ========== 统一训练和预测接口 ==========
+
+ train(trainingData) {
+ if (!Array.isArray(trainingData) || trainingData.length === 0) {
+ console.warn('⚠️ No training data provided');
+ return false;
+ }
+
+ const processedData = trainingData.map(sample => ({
+ features: sample.features || this.extractFeatures(sample.tab || sample),
+ label: sample.label
+ }));
+
+ this.trainingData = [
+ ...this.trainingData,
+ ...processedData
+ ].slice(-this.maxTrainingSamples);
+
+ switch (this.modelType) {
+ case 'logistic':
+ this.trainLogisticRegression(this.trainingData);
+ break;
+ case 'decision-tree':
+ this.trainDecisionTree(this.trainingData);
+ break;
+ case 'naive-bayes':
+ default:
+ this.trainNaiveBayes(this.trainingData);
+ break;
+ }
+
+ return true;
+ }
+
+ predict(tab) {
+ const features = this.extractFeatures(tab);
+
+ switch (this.modelType) {
+ case 'logistic':
+ return this.predictLogisticRegression(features);
+ case 'decision-tree':
+ return this.predictDecisionTree(features);
+ case 'naive-bayes':
+ default:
+ return this.predictNaiveBayes(features);
+ }
+ }
+
+ predictTopK(tab, k = 3) {
+ const features = this.extractFeatures(tab);
+ const predictions = [];
+
+ if (!this.trained || this.classCounts.size === 0) {
+ return [];
+ }
+
+ const totalSamples = Array.from(this.classCounts.values()).reduce((a, b) => a + b, 0);
+ const vocabSize = this.vocabulary.size;
+
+ for (const [label, count] of this.classCounts.entries()) {
+ const prior = Math.log(count / totalSamples);
+
+ let likelihood = 0;
+ const wordCounts = this.classWordCounts.get(label);
+ const totalWords = wordCounts ? Array.from(wordCounts.values()).reduce((a, b) => a + b, 0) : 0;
+
+ for (const token of features.tokens) {
+ const wordCount = wordCounts ? (wordCounts.get(token) || 0) : 0;
+ likelihood += Math.log((wordCount + 1) / (totalWords + vocabSize));
+ }
+
+ const score = prior + likelihood;
+ predictions.push({ label, score });
+ }
+
+ return predictions
+ .sort((a, b) => b.score - a.score)
+ .slice(0, k);
+ }
+
+ // ========== 在线学习 (增量训练) ==========
+
+ learn(tab, label) {
+ const features = this.extractFeatures(tab);
+
+ this.trainingData.push({ features, label });
+ if (this.trainingData.length > this.maxTrainingSamples) {
+ this.trainingData.shift();
+ }
+
+ if (!this.classCounts.has(label)) {
+ this.classCounts.set(label, 0);
+ this.classWordCounts.set(label, new Map());
+ }
+
+ this.classCounts.set(label, (this.classCounts.get(label) || 0) + 1);
+
+ const wordCounts = this.classWordCounts.get(label);
+ for (const token of features.tokens) {
+ this.vocabulary.set(token, (this.vocabulary.get(token) || 0) + 1);
+ wordCounts.set(token, (wordCounts.get(token) || 0) + 1);
+ }
+
+ this.trained = true;
+ console.log('📚 Online learning: added sample for label', label);
+ }
+
+ // ========== 模型持久化 ==========
+
+ async save(storageManager) {
+ const modelData = {
+ modelType: this.modelType,
+ vocabulary: Object.fromEntries(this.vocabulary),
+ classCounts: Object.fromEntries(this.classCounts),
+ classWordCounts: Object.fromEntries(
+ Array.from(this.classWordCounts.entries()).map(([k, v]) => [k, Object.fromEntries(v)])
+ ),
+ weights: Object.fromEntries(this.weights),
+ trained: this.trained,
+ trainingData: this.trainingData,
+ featureIndex: this.featureIndex ? Object.fromEntries(this.featureIndex) : null,
+ labels: this.labels,
+ decisionTree: this.decisionTree
+ };
+
+ try {
+ if (storageManager && typeof storageManager.setStorageData === 'function') {
+ await storageManager.setStorageData(this.storageKey, modelData);
+ } else if (chrome && chrome.storage && chrome.storage.local) {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ [this.storageKey]: modelData }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+ console.log('💾 ML model saved');
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to save ML model:', error);
+ return false;
+ }
+ }
+
+ async load(storageManager) {
+ try {
+ let modelData;
+
+ if (storageManager && typeof storageManager.getSettings === 'function') {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get(this.storageKey, (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ modelData = result[this.storageKey];
+ } else if (chrome && chrome.storage && chrome.storage.local) {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get(this.storageKey, (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ modelData = result[this.storageKey];
+ }
+
+ if (modelData) {
+ this.modelType = modelData.modelType || this.modelType;
+ this.vocabulary = new Map(Object.entries(modelData.vocabulary || {}));
+ this.classCounts = new Map(Object.entries(modelData.classCounts || {}));
+ this.classWordCounts = new Map(
+ Object.entries(modelData.classWordCounts || {}).map(([k, v]) => [k, new Map(Object.entries(v))])
+ );
+ this.weights = new Map(Object.entries(modelData.weights || {}));
+ this.trained = modelData.trained || false;
+ this.isTrained = this.trained;
+ this.hasModel = this.trained && this.classCounts.size > 0;
+ this.trainingData = modelData.trainingData || [];
+ if (modelData.featureIndex) {
+ this.featureIndex = new Map(Object.entries(modelData.featureIndex));
+ }
+ this.labels = modelData.labels;
+ this.decisionTree = modelData.decisionTree;
+
+ console.log('📥 ML model loaded with', this.classCounts.size, 'classes, trained:', this.isTrained);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.warn('⚠️ Failed to load ML model:', error);
+ return false;
+ }
+ }
+
+ getStats() {
+ return {
+ modelType: this.modelType,
+ trained: this.trained,
+ vocabularySize: this.vocabulary.size,
+ classCount: this.classCounts.size,
+ trainingSampleCount: this.trainingData.length,
+ classes: Array.from(this.classCounts.entries())
+ };
+ }
+
+ reset() {
+ this.vocabulary.clear();
+ this.classCounts.clear();
+ this.classWordCounts.clear();
+ this.weights.clear();
+ this.trained = false;
+ this.isTrained = false;
+ this.hasModel = false;
+ this.trainingData = [];
+ this.featureIndex = null;
+ this.labels = null;
+ this.decisionTree = null;
+ console.log('🔄 ML classifier reset');
+ }
+}
+
+class EnsembleClassifier {
+ constructor(options = {}) {
+ this.classifiers = {
+ naiveBayes: new MLClassifier({ modelType: 'naive-bayes', ...options }),
+ logistic: new MLClassifier({ modelType: 'logistic', ...options }),
+ decisionTree: new MLClassifier({ modelType: 'decision-tree', ...options })
+ };
+ this.weights = options.weights || {
+ naiveBayes: 0.4,
+ logistic: 0.35,
+ decisionTree: 0.25
+ };
+ }
+
+ train(trainingData) {
+ let success = true;
+ for (const [name, classifier] of Object.entries(this.classifiers)) {
+ const result = classifier.train(trainingData);
+ if (!result) success = false;
+ }
+ return success;
+ }
+
+ predict(tab) {
+ const votes = new Map();
+ const confidences = new Map();
+
+ for (const [name, classifier] of Object.entries(this.classifiers)) {
+ const result = classifier.predict(tab);
+ if (result && result.label) {
+ const weight = this.weights[name] || 1;
+ const currentWeight = votes.get(result.label) || 0;
+ votes.set(result.label, currentWeight + weight);
+
+ const score = result.score || 0.5;
+ const currentConfidence = confidences.get(result.label) || 0;
+ confidences.set(result.label, currentConfidence + (score * weight));
+ }
+ }
+
+ let bestLabel = null;
+ let bestScore = 0;
+
+ for (const [label, vote] of votes.entries()) {
+ if (vote > bestScore) {
+ bestScore = vote;
+ bestLabel = label;
+ }
+ }
+
+ return {
+ label: bestLabel,
+ score: bestScore,
+ votes: Object.fromEntries(votes),
+ confidences: Object.fromEntries(confidences)
+ };
+ }
+
+ predictTopK(tab, k = 3) {
+ const allPredictions = [];
+
+ for (const [name, classifier] of Object.entries(this.classifiers)) {
+ const predictions = classifier.predictTopK(tab, k);
+ const weight = this.weights[name] || 1;
+
+ for (const pred of predictions) {
+ allPredictions.push({
+ ...pred,
+ score: pred.score * weight,
+ classifier: name
+ });
+ }
+ }
+
+ const aggregated = new Map();
+ for (const pred of allPredictions) {
+ const current = aggregated.get(pred.label) || 0;
+ aggregated.set(pred.label, current + pred.score);
+ }
+
+ return Array.from(aggregated.entries())
+ .map(([label, score]) => ({ label, score }))
+ .sort((a, b) => b.score - a.score)
+ .slice(0, k);
+ }
+
+ async save(storageManager) {
+ for (const [name, classifier] of Object.entries(this.classifiers)) {
+ await classifier.save(storageManager);
+ }
+ }
+
+ async load(storageManager) {
+ for (const [name, classifier] of Object.entries(this.classifiers)) {
+ await classifier.load(storageManager);
+ }
+ }
+
+ learn(tab, label) {
+ for (const classifier of Object.values(this.classifiers)) {
+ classifier.learn(tab, label);
+ }
+ }
+
+ getStats() {
+ const stats = {};
+ for (const [name, classifier] of Object.entries(this.classifiers)) {
+ stats[name] = classifier.getStats();
+ }
+ return stats;
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { MLClassifier, EnsembleClassifier };
+}
+
+if (typeof window !== 'undefined') {
+ window.MLClassifier = MLClassifier;
+ window.EnsembleClassifier = EnsembleClassifier;
+}
diff --git a/utils/multi-window-sync.js b/utils/multi-window-sync.js
new file mode 100644
index 0000000..c8ee827
--- /dev/null
+++ b/utils/multi-window-sync.js
@@ -0,0 +1,748 @@
+/**
+ * MultiWindowSync - 跨窗口同步管理器
+ *
+ * 解决多窗口标签页跨窗口分组联动难题,实现:
+ * - 跨窗口实时同步分组状态
+ * - 窗口间事件广播与监听
+ * - 标签页移动同步
+ * - 分组操作同步
+ * - 冲突检测与解决
+ */
+
+class MultiWindowSync {
+ constructor(storageManager, eventBus, options = {}) {
+ this.storageManager = storageManager;
+ this.eventBus = eventBus;
+
+ this.windowId = options.windowId || this.generateWindowId();
+ this.sessionId = options.sessionId || this.generateSessionId();
+
+ this.messageChannel = 'tabflow:multi_window_sync';
+ this.heartbeatInterval = options.heartbeatInterval || 5000;
+ this.staleWindowThreshold = options.staleWindowThreshold || 15000;
+
+ this.windows = new Map();
+ this.currentWindowInfo = {
+ windowId: this.windowId,
+ sessionId: this.sessionId,
+ tabCount: 0,
+ activeTabId: null,
+ lastHeartbeat: Date.now(),
+ isFocused: false,
+ createdAt: Date.now()
+ };
+
+ this.heartbeatTimer = null;
+ this.cleanupTimer = null;
+
+ this.listeners = new Map();
+ this.pendingMessages = [];
+
+ this.isInitialized = false;
+ this.isListening = false;
+
+ console.log('🪟 MultiWindowSync initialized, windowId:', this.windowId);
+ }
+
+ generateWindowId() {
+ return `win_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
+ }
+
+ generateSessionId() {
+ return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ // ========== 初始化 ==========
+
+ async initialize() {
+ if (this.isInitialized) return true;
+
+ this.setupEventListeners();
+ this.startHeartbeat();
+ this.startCleanup();
+
+ await this.registerWindow();
+ await this.syncWithOtherWindows();
+
+ this.isInitialized = true;
+ console.log('✅ MultiWindowSync initialized');
+
+ return true;
+ }
+
+ setupEventListeners() {
+ if (this.isListening) return;
+
+ chrome.storage.onChanged.addListener((changes, areaName) => {
+ if (areaName === 'local') {
+ this.handleStorageChanges(changes);
+ }
+ });
+
+ if (chrome.runtime && chrome.runtime.onMessage) {
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message.type === this.messageChannel) {
+ this.handleMessage(message.payload, sender);
+ if (typeof sendResponse === 'function') {
+ sendResponse({ received: true, windowId: this.windowId });
+ }
+ }
+ return true;
+ });
+ }
+
+ if (chrome.windows) {
+ chrome.windows.onFocusChanged.addListener((windowId) => {
+ this.handleWindowFocusChanged(windowId);
+ });
+
+ chrome.windows.onRemoved.addListener((windowId) => {
+ this.handleWindowRemoved(windowId);
+ });
+
+ chrome.windows.onCreated.addListener((window) => {
+ this.handleWindowCreated(window);
+ });
+ }
+
+ this.isListening = true;
+ console.log('📡 Event listeners setup for multi-window sync');
+ }
+
+ // ========== 心跳机制 ==========
+
+ startHeartbeat() {
+ if (this.heartbeatTimer) {
+ clearInterval(this.heartbeatTimer);
+ }
+
+ this.heartbeatTimer = setInterval(() => {
+ this.sendHeartbeat();
+ }, this.heartbeatInterval);
+
+ this.sendHeartbeat();
+ }
+
+ stopHeartbeat() {
+ if (this.heartbeatTimer) {
+ clearInterval(this.heartbeatTimer);
+ this.heartbeatTimer = null;
+ }
+ }
+
+ async sendHeartbeat() {
+ this.currentWindowInfo.lastHeartbeat = Date.now();
+
+ try {
+ const tabs = await this.getWindowTabs();
+ this.currentWindowInfo.tabCount = tabs.length;
+
+ const activeTab = await this.getActiveTab();
+ this.currentWindowInfo.activeTabId = activeTab ? activeTab.id : null;
+ } catch (error) {
+ console.warn('⚠️ Failed to update window info:', error);
+ }
+
+ this.windows.set(this.windowId, { ...this.currentWindowInfo });
+ await this.saveWindows();
+ }
+
+ // ========== 窗口管理 ==========
+
+ async registerWindow() {
+ const windowInfo = {
+ windowId: this.windowId,
+ sessionId: this.sessionId,
+ tabCount: 0,
+ activeTabId: null,
+ lastHeartbeat: Date.now(),
+ isFocused: false,
+ createdAt: Date.now()
+ };
+
+ this.windows.set(this.windowId, windowInfo);
+ await this.saveWindows();
+
+ await this.broadcast({
+ action: 'window_registered',
+ windowId: this.windowId,
+ windowInfo
+ });
+
+ console.log('📝 Window registered:', this.windowId);
+ }
+
+ async unregisterWindow() {
+ this.windows.delete(this.windowId);
+ await this.saveWindows();
+
+ await this.broadcast({
+ action: 'window_unregistered',
+ windowId: this.windowId
+ });
+
+ this.stopHeartbeat();
+ this.stopCleanup();
+
+ console.log('📋 Window unregistered:', this.windowId);
+ }
+
+ async saveWindows() {
+ try {
+ const windowsData = Object.fromEntries(this.windows);
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ 'tabflow:windows': windowsData }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ console.error('❌ Failed to save windows:', error);
+ }
+ }
+
+ async loadWindows() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get('tabflow:windows', (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ const windowsData = result['tabflow:windows'] || {};
+ this.windows = new Map(Object.entries(windowsData));
+ } catch (error) {
+ console.warn('⚠️ Failed to load windows:', error);
+ this.windows = new Map();
+ }
+ }
+
+ // ========== 过期窗口清理 ==========
+
+ startCleanup() {
+ if (this.cleanupTimer) {
+ clearInterval(this.cleanupTimer);
+ }
+
+ this.cleanupTimer = setInterval(() => {
+ this.cleanupStaleWindows();
+ }, this.staleWindowThreshold);
+ }
+
+ stopCleanup() {
+ if (this.cleanupTimer) {
+ clearInterval(this.cleanupTimer);
+ this.cleanupTimer = null;
+ }
+ }
+
+ async cleanupStaleWindows() {
+ const now = Date.now();
+ const staleWindows = [];
+
+ for (const [windowId, windowInfo] of this.windows.entries()) {
+ const timeSinceHeartbeat = now - (windowInfo.lastHeartbeat || 0);
+
+ if (timeSinceHeartbeat > this.staleWindowThreshold && windowId !== this.windowId) {
+ staleWindows.push(windowId);
+ }
+ }
+
+ if (staleWindows.length > 0) {
+ console.log('🧹 Cleaning up stale windows:', staleWindows);
+
+ for (const windowId of staleWindows) {
+ this.windows.delete(windowId);
+ }
+
+ await this.saveWindows();
+
+ for (const windowId of staleWindows) {
+ this.notify('window_stale', { windowId });
+ }
+ }
+ }
+
+ // ========== 消息广播系统 ==========
+
+ async broadcast(payload) {
+ const message = {
+ type: this.messageChannel,
+ payload: {
+ ...payload,
+ fromWindowId: this.windowId,
+ fromSessionId: this.sessionId,
+ timestamp: Date.now()
+ }
+ };
+
+ try {
+ if (chrome.runtime && chrome.runtime.sendMessage) {
+ await chrome.runtime.sendMessage(message);
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to broadcast message:', error);
+ }
+
+ try {
+ const messages = await this.loadPendingMessages();
+ messages.push(message.payload);
+
+ if (messages.length > 100) {
+ messages.splice(0, messages.length - 50);
+ }
+
+ await this.savePendingMessages(messages);
+ } catch (error) {
+ console.warn('⚠️ Failed to save pending message:', error);
+ }
+
+ console.log('📢 Broadcasted:', payload.action || 'message');
+ }
+
+ handleMessage(payload, sender) {
+ if (payload.fromWindowId === this.windowId) {
+ return;
+ }
+
+ console.log('📨 Received message from window:', payload.fromWindowId, payload.action);
+
+ switch (payload.action) {
+ case 'window_registered':
+ this.handleWindowRegistered(payload);
+ break;
+ case 'window_unregistered':
+ this.handleWindowUnregistered(payload);
+ break;
+ case 'tab_created':
+ this.handleRemoteTabCreated(payload);
+ break;
+ case 'tab_removed':
+ this.handleRemoteTabRemoved(payload);
+ break;
+ case 'tab_updated':
+ this.handleRemoteTabUpdated(payload);
+ break;
+ case 'tab_moved_to_group':
+ this.handleRemoteTabMoved(payload);
+ break;
+ case 'group_created':
+ this.handleRemoteGroupCreated(payload);
+ break;
+ case 'group_updated':
+ this.handleRemoteGroupUpdated(payload);
+ break;
+ case 'group_deleted':
+ this.handleRemoteGroupDeleted(payload);
+ break;
+ case 'sync_request':
+ this.handleSyncRequest(payload);
+ break;
+ case 'sync_response':
+ this.handleSyncResponse(payload);
+ break;
+ case 'heartbeat':
+ this.handleRemoteHeartbeat(payload);
+ break;
+ default:
+ this.notify('custom_message', payload);
+ }
+ }
+
+ // ========== 存储变更监听 ==========
+
+ handleStorageChanges(changes) {
+ if (changes['tabflow:windows']) {
+ const oldWindows = changes['tabflow:windows'].oldValue || {};
+ const newWindows = changes['tabflow:windows'].newValue || {};
+
+ for (const [windowId, windowInfo] of Object.entries(newWindows)) {
+ if (!oldWindows[windowId] && windowId !== this.windowId) {
+ this.handleWindowRegistered({ windowId, windowInfo });
+ }
+ }
+
+ for (const windowId of Object.keys(oldWindows)) {
+ if (!newWindows[windowId] && windowId !== this.windowId) {
+ this.handleWindowUnregistered({ windowId });
+ }
+ }
+ }
+ }
+
+ // ========== 窗口事件处理 ==========
+
+ handleWindowRegistered(payload) {
+ const { windowId, windowInfo } = payload;
+
+ if (!this.windows.has(windowId)) {
+ this.windows.set(windowId, windowInfo);
+ }
+
+ this.notify('window_registered', { windowId, windowInfo });
+ console.log('🪟 Window registered:', windowId);
+ }
+
+ handleWindowUnregistered(payload) {
+ const { windowId } = payload;
+ this.windows.delete(windowId);
+
+ this.notify('window_unregistered', { windowId });
+ console.log('🪟 Window unregistered:', windowId);
+ }
+
+ handleWindowFocusChanged(windowId) {
+ if (windowId === chrome.windows.WINDOW_ID_NONE) {
+ this.currentWindowInfo.isFocused = false;
+ return;
+ }
+
+ this.getOwnWindowId().then(ownWindowId => {
+ this.currentWindowInfo.isFocused = ownWindowId === windowId;
+
+ if (this.currentWindowInfo.isFocused) {
+ this.notify('window_focused', { windowId: ownWindowId });
+ } else {
+ this.notify('window_blurred', { windowId: ownWindowId });
+ }
+ });
+ }
+
+ handleWindowRemoved(windowId) {
+ this.windows.delete(windowId.toString());
+ this.saveWindows();
+ this.notify('window_removed', { windowId });
+ }
+
+ handleWindowCreated(window) {
+ this.notify('window_created', { window });
+ }
+
+ handleRemoteHeartbeat(payload) {
+ const { fromWindowId } = payload;
+
+ if (this.windows.has(fromWindowId)) {
+ const windowInfo = this.windows.get(fromWindowId);
+ windowInfo.lastHeartbeat = Date.now();
+ }
+ }
+
+ // ========== 标签页事件同步 ==========
+
+ async syncWithOtherWindows() {
+ await this.loadWindows();
+
+ const otherWindows = this.getOtherWindows();
+ if (otherWindows.length > 0) {
+ console.log('🔄 Syncing with', otherWindows.length, 'other windows');
+
+ await this.broadcast({
+ action: 'sync_request',
+ requestingWindowId: this.windowId
+ });
+ }
+ }
+
+ handleSyncRequest(payload) {
+ const { requestingWindowId } = payload;
+
+ this.sendSyncResponse(requestingWindowId);
+ }
+
+ async sendSyncResponse(targetWindowId) {
+ const groups = await this.storageManager.getAllGroups();
+ const tabs = await this.storageManager.getAllTabs();
+
+ await this.broadcast({
+ action: 'sync_response',
+ targetWindowId,
+ groups,
+ tabs,
+ windowId: this.windowId
+ });
+ }
+
+ handleSyncResponse(payload) {
+ if (payload.targetWindowId !== this.windowId) {
+ return;
+ }
+
+ this.notify('sync_received', {
+ fromWindowId: payload.windowId,
+ groups: payload.groups,
+ tabs: payload.tabs
+ });
+ }
+
+ // ========== 标签页操作同步 ==========
+
+ async notifyTabCreated(tab) {
+ await this.broadcast({
+ action: 'tab_created',
+ tab: {
+ id: tab.id,
+ uuid: tab.uuid,
+ url: tab.url,
+ title: tab.title,
+ groupId: tab.groupId,
+ windowId: tab.windowId
+ }
+ });
+ }
+
+ handleRemoteTabCreated(payload) {
+ this.notify('remote_tab_created', payload);
+ }
+
+ async notifyTabRemoved(tabId, tabData) {
+ await this.broadcast({
+ action: 'tab_removed',
+ tabId,
+ tabData: tabData ? {
+ id: tabData.id,
+ uuid: tabData.uuid,
+ groupId: tabData.groupId
+ } : null
+ });
+ }
+
+ handleRemoteTabRemoved(payload) {
+ this.notify('remote_tab_removed', payload);
+ }
+
+ async notifyTabUpdated(tab) {
+ await this.broadcast({
+ action: 'tab_updated',
+ tab: {
+ id: tab.id,
+ uuid: tab.uuid,
+ url: tab.url,
+ title: tab.title,
+ groupId: tab.groupId,
+ favicon: tab.favicon
+ }
+ });
+ }
+
+ handleRemoteTabUpdated(payload) {
+ this.notify('remote_tab_updated', payload);
+ }
+
+ // ========== 分组操作同步 ==========
+
+ async notifyTabMovedToGroup(tab, groupId, oldGroupId = null) {
+ await this.broadcast({
+ action: 'tab_moved_to_group',
+ tab: {
+ id: tab.id,
+ uuid: tab.uuid
+ },
+ groupId,
+ oldGroupId
+ });
+ }
+
+ handleRemoteTabMoved(payload) {
+ this.notify('remote_tab_moved', payload);
+ }
+
+ async notifyGroupCreated(group) {
+ await this.broadcast({
+ action: 'group_created',
+ group: {
+ id: group.id,
+ name: group.name,
+ type: group.type,
+ color: group.color,
+ createdAt: group.createdAt
+ }
+ });
+ }
+
+ handleRemoteGroupCreated(payload) {
+ this.notify('remote_group_created', payload);
+ }
+
+ async notifyGroupUpdated(group) {
+ await this.broadcast({
+ action: 'group_updated',
+ group: {
+ id: group.id,
+ name: group.name,
+ color: group.color,
+ collapsed: group.collapsed,
+ updatedAt: Date.now()
+ }
+ });
+ }
+
+ handleRemoteGroupUpdated(payload) {
+ this.notify('remote_group_updated', payload);
+ }
+
+ async notifyGroupDeleted(groupId, tabIds = []) {
+ await this.broadcast({
+ action: 'group_deleted',
+ groupId,
+ tabIds
+ });
+ }
+
+ handleRemoteGroupDeleted(payload) {
+ this.notify('remote_group_deleted', payload);
+ }
+
+ // ========== 辅助方法 ==========
+
+ async getOwnWindowId() {
+ try {
+ if (chrome && chrome.windows && chrome.windows.getCurrent) {
+ const window = await chrome.windows.getCurrent();
+ return window.id;
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to get current window ID:', error);
+ }
+ return null;
+ }
+
+ async getWindowTabs() {
+ try {
+ if (chrome && chrome.tabs && chrome.tabs.query) {
+ const currentWindow = await chrome.windows.getCurrent();
+ return await chrome.tabs.query({ windowId: currentWindow.id });
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to get window tabs:', error);
+ }
+ return [];
+ }
+
+ async getActiveTab() {
+ try {
+ if (chrome && chrome.tabs && chrome.tabs.query) {
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
+ return tabs[0] || null;
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to get active tab:', error);
+ }
+ return null;
+ }
+
+ getOtherWindows() {
+ const windows = [];
+ for (const [windowId, windowInfo] of this.windows.entries()) {
+ if (windowId !== this.windowId) {
+ windows.push({ windowId, ...windowInfo });
+ }
+ }
+ return windows;
+ }
+
+ async loadPendingMessages() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get('tabflow:pending_messages', (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ return result['tabflow:pending_messages'] || [];
+ } catch (error) {
+ console.warn('⚠️ Failed to load pending messages:', error);
+ return [];
+ }
+ }
+
+ async savePendingMessages(messages) {
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ 'tabflow:pending_messages': messages }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ console.error('❌ Failed to save pending messages:', error);
+ }
+ }
+
+ // ========== 事件监听系统 ==========
+
+ on(event, callback) {
+ if (!this.listeners.has(event)) {
+ this.listeners.set(event, []);
+ }
+ this.listeners.get(event).push(callback);
+ }
+
+ off(event, callback) {
+ if (!this.listeners.has(event)) return;
+
+ const callbacks = this.listeners.get(event);
+ const index = callbacks.indexOf(callback);
+ if (index > -1) {
+ callbacks.splice(index, 1);
+ }
+ }
+
+ notify(event, data) {
+ if (!this.listeners.has(event)) return;
+
+ const callbacks = this.listeners.get(event);
+ for (const callback of callbacks) {
+ try {
+ callback(data);
+ } catch (error) {
+ console.error(`❌ Error in ${event} listener:`, error);
+ }
+ }
+ }
+
+ // ========== 统计信息 ==========
+
+ getStats() {
+ return {
+ windowId: this.windowId,
+ sessionId: this.sessionId,
+ initialized: this.isInitialized,
+ listening: this.isListening,
+ totalWindows: this.windows.size,
+ otherWindows: this.getOtherWindows().length,
+ currentWindowInfo: { ...this.currentWindowInfo }
+ };
+ }
+
+ async shutdown() {
+ this.stopHeartbeat();
+ this.stopCleanup();
+ await this.unregisterWindow();
+
+ this.isInitialized = false;
+ this.isListening = false;
+
+ console.log('🔌 MultiWindowSync shutdown complete');
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = MultiWindowSync;
+}
+
+if (typeof window !== 'undefined') {
+ window.MultiWindowSync = MultiWindowSync;
+}
diff --git a/utils/scene-classifier.js b/utils/scene-classifier.js
new file mode 100644
index 0000000..3b8484e
--- /dev/null
+++ b/utils/scene-classifier.js
@@ -0,0 +1,858 @@
+/**
+ * SceneClassifier - 场景分类器
+ *
+ * 识别访问场景:
+ * - 工作场景 (办公、协作、邮件、会议)
+ * - 学习场景 (教程、课程、文档、研究)
+ * - 娱乐场景 (视频、游戏、社交、购物)
+ * - 生活场景 (金融、健康、旅行、美食)
+ * - 开发场景 (代码、API、技术文档)
+ *
+ * 支持:
+ * - 多维度自动归类
+ * - 用户自定义归类权重
+ * - 手动调整分类优先级
+ */
+
+class SceneClassifier {
+ constructor(options = {}) {
+ this.customScenePatterns = new Map();
+ this.scenePriorities = new Map();
+ this.userSceneWeights = new Map();
+
+ this.defaultPriorities = {
+ work: 5,
+ development: 4,
+ learning: 3,
+ entertainment: 2,
+ life: 1,
+ other: 0
+ };
+
+ for (const [scene, priority] of Object.entries(this.defaultPriorities)) {
+ this.scenePriorities.set(scene, priority);
+ }
+
+ this.scenePatterns = {
+ work: {
+ name: '工作',
+ icon: '💼',
+ color: '#1976D2',
+ description: '办公协作、邮件会议、商务沟通',
+ patterns: {
+ domains: [
+ 'office.com', 'office365.com', 'microsoft365.com',
+ 'outlook.com', 'gmail.com', 'yahoo.com', 'hotmail.com',
+ 'zoom.us', 'teams.microsoft.com', 'meet.google.com',
+ 'webex.com', 'gotomeeting.com', 'slack.com', 'discord.com',
+ 'jira.atlassian.com', 'confluence.atlassian.com',
+ 'trello.com', 'asana.com', 'notion.so', 'basecamp.com',
+ 'clickup.com', 'monday.com', 'wrike.com', 'todoist.com',
+ 'feishu.cn', 'larksuite.com', 'dingtalk.com',
+ 'qy.weixin.qq.com', 'work.weixin.qq.com',
+ 'wps.cn', 'kdocs.cn', 'yunshangxiezuo.com',
+ 'teambition.com', 'coding.net', 'tower.im',
+ 'salesforce.com', 'hubspot.com', 'zendesk.com',
+ 'intercom.com', 'freshworks.com', 'zoho.com',
+ 'linkedin.com', 'xing.com', 'glassdoor.com',
+ 'indeed.com', 'monster.com', 'careerbuilder.com',
+ 'zhaopin.com', 'liepin.com', 'lagou.com', 'mokahr.com',
+ '51job.com', 'zhipin.com', 'kanzhun.com',
+ 'dianping.com', 'meituan.com', 'ele.me',
+ 'dingtalk.com', 'aliwork.com', 'youzan.com',
+ 'weidian.com', 'xiaoe-tech.com', 'koudaitong.com',
+ 'weimob.com', 'jinritemai.com'
+ ],
+ pathPatterns: [
+ '/mail', '/email', '/inbox', '/message',
+ '/meeting', '/meet', '/call', '/conference', '/calendar',
+ '/task', '/project', '/board', '/workspace', '/team',
+ '/office', '/work', '/business', '/company', '/corp',
+ '/hr', '/recruit', '/career', '/job', '/resume',
+ '/admin', '/manage', '/settings', '/dashboard',
+ '/report', '/analytics', '/stats', '/crm', '/erp'
+ ],
+ titleKeywords: [
+ '邮件', 'email', 'mail', 'inbox', '消息', 'message',
+ '会议', 'meeting', 'zoom', 'teams', '日程', 'calendar',
+ '任务', 'task', '项目', 'project', '看板', 'board',
+ '工作', 'work', 'office', '办公', '商务', 'business',
+ '公司', 'company', '企业', 'corp', '团队', 'team',
+ '协作', 'collaboration', 'OA', '系统', 'system',
+ '管理', 'manage', '后台', 'admin', '控制台', 'console',
+ '报表', 'report', '分析', 'analytics', '数据', 'data',
+ '招聘', 'recruit', '求职', 'job', '简历', 'resume',
+ '面试', 'interview', 'offer', '入职', 'onboarding'
+ ],
+ timePatterns: {
+ weekday: [9, 10, 11, 14, 15, 16, 17],
+ weekend: []
+ }
+ },
+ weight: 1.0
+ },
+
+ development: {
+ name: '开发',
+ icon: '💻',
+ color: '#7B1FA2',
+ description: '代码开发、API文档、技术学习',
+ patterns: {
+ domains: [
+ 'github.com', 'gitlab.com', 'bitbucket.org',
+ 'gitee.com', 'coding.net', 'gitcode.com', 'gitea.io',
+ 'stackoverflow.com', 'stackexchange.com', 'serverfault.com',
+ 'superuser.com', 'askubuntu.com', 'unix.stackexchange.com',
+ 'npmjs.com', 'npmjs.org', 'yarnpkg.com', 'pnpm.io',
+ 'pypi.org', 'pip.pypa.io', 'rubygems.org', 'crates.io',
+ 'pub.dev', 'maven.apache.org', 'mvnrepository.com',
+ 'nuget.org', 'packagist.org', 'go.dev', 'pkg.go.dev',
+ 'deno.land', 'nodejs.org', 'python.org', 'java.com',
+ 'jetbrains.com', 'visualstudio.com', 'code.visualstudio.com',
+ 'docker.com', 'hub.docker.com', 'kubernetes.io',
+ 'amazon.com', 'aws.amazon.com', 'console.aws.amazon.com',
+ 'azure.com', 'portal.azure.com', 'cloud.google.com',
+ 'cloud.tencent.com', 'console.cloud.tencent.com',
+ 'aliyun.com', 'developer.aliyun.com',
+ 'huaweicloud.com', 'developer.huaweicloud.com',
+ 'developer.mozilla.org', 'mdn.io', 'w3schools.com',
+ 'w3.org', 'caniuse.com', 'caniuse.dev',
+ 'babeljs.io', 'webpack.js.org', 'vitejs.dev',
+ 'rollupjs.org', 'parceljs.org', 'jestjs.io',
+ 'testing-library.com', 'cypress.io', 'playwright.dev',
+ 'postman.com', 'insomnia.rest', 'swagger.io', 'openapi.org',
+ 'graphql.org', 'apollographql.com', 'reactjs.org',
+ 'vuejs.org', 'cn.vuejs.org', 'angular.io', 'angular.cn',
+ 'svelte.dev', 'nextjs.org', 'nuxt.com', 'gatsbyjs.com',
+ 'remix.run', 'tailwindcss.com', 'getbootstrap.com',
+ 'mui.com', 'ant.design', 'element.eleme.io',
+ 'arco.design', 'tdesign.tencent.com',
+ 'juejin.cn', 'segmentfault.com', 'oschina.net',
+ 'v2ex.com', 'cnblogs.com', 'csdn.net', 'itpub.net',
+ '51cto.com', 'ibm.com/developerworks',
+ 'infoq.com', 'infoq.cn', '36kr.com', 'huxiu.com',
+ 'geekbang.org', 'time.geekbang.org',
+ '极客时间', '极客邦', 'tutorialspoint.com',
+ 'digitalocean.com', 'linode.com', 'heroku.com',
+ 'vercel.com', 'netlify.com', 'cloudflare.com',
+ 'fastly.com', 'nginx.org', 'apache.org',
+ 'mysql.com', 'postgresql.org', 'mongodb.com',
+ 'redis.io', 'elasticsearch.org', 'rabbitmq.com',
+ 'kafka.apache.org', 'zookeeper.apache.org',
+ 'prometheus.io', 'grafana.com', 'datadoghq.com',
+ 'newrelic.com', 'sentry.io', 'bugsnag.com',
+ 'terraform.io', 'ansible.com', 'puppet.com',
+ 'chef.io', 'saltproject.org', 'jenkins.io',
+ 'gitlab-ci', 'circleci.com', 'travis-ci.org',
+ 'github.io', 'pages.dev', 'netlify.app'
+ ],
+ pathPatterns: [
+ '/code', '/src', '/source', '/repo', '/repository',
+ '/api', '/docs', '/documentation', '/developers',
+ '/developer', '/dev', '/console', '/admin',
+ '/pipeline', '/ci', '/cd', '/build', '/deploy',
+ '/test', '/debug', '/log', '/logs', '/monitor',
+ '/issue', '/issues', '/pull', '/pr', '/merge',
+ '/commit', '/branch', '/tag', '/release', '/wiki',
+ '/tutorial', '/guide', '/learn', '/course',
+ '/framework', '/library', '/package', '/module',
+ '/plugin', '/extension', '/api-reference', '/sdk'
+ ],
+ titleKeywords: [
+ '代码', 'code', '源码', 'source', '编程', 'program',
+ '开发', 'developer', '开发', 'dev', '技术', 'tech',
+ 'API', '接口', '文档', 'docs', 'documentation',
+ '框架', 'framework', '库', 'library', '包', 'package',
+ '部署', 'deploy', '构建', 'build', '测试', 'test',
+ '调试', 'debug', 'Git', 'GitHub', 'GitLab', 'Gitee',
+ 'Docker', 'Kubernetes', '容器', 'container',
+ '服务器', 'server', '云服务', 'cloud', '数据库',
+ 'mysql', 'postgres', 'mongodb', 'redis', '缓存',
+ '前端', 'frontend', '后端', 'backend', '全栈', 'fullstack',
+ '架构', 'architecture', '设计模式', 'design pattern',
+ '算法', 'algorithm', '数据结构', 'data structure',
+ '性能优化', 'performance', '安全', 'security', '网络',
+ '操作系统', 'OS', 'Linux', 'Unix', 'Windows', 'macOS'
+ ],
+ timePatterns: {
+ weekday: [10, 11, 14, 15, 16, 19, 20, 21],
+ weekend: [10, 11, 14, 15, 16, 19, 20, 21, 22]
+ }
+ },
+ weight: 1.0
+ },
+
+ learning: {
+ name: '学习',
+ icon: '📚',
+ color: '#388E3C',
+ description: '课程学习、教程文档、研究阅读',
+ patterns: {
+ domains: [
+ 'coursera.org', 'edx.org', 'udemy.com', 'lynda.com',
+ 'pluralsight.com', 'skillshare.com', 'masterclass.com',
+ 'khanacademy.org', 'codecademy.com', 'freecodecamp.org',
+ 'theodinproject.com', 'sololearn.com', 'datacamp.com',
+ 'teamtreehouse.com', 'codewars.com', 'hackerrank.com',
+ 'leetcode.com', 'lintcode.com', 'nowcoder.com',
+ 'acwing.com', 'pintia.cn', 'openjudge.cn',
+ 'edx.org', 'classcentral.com', 'mooc.cn',
+ 'icourse163.org', 'icourses.cn', 'xuetangx.com',
+ 'chinesemooc.org', 'eol.cn', 'study.163.com',
+ 'ke.qq.com', 'class.hujiang.com', 'hujiang.com',
+ 'tmooc.cn', 'zhihuishu.com', 'chaoxing.com',
+ 'fanya.chaoxing.com', 'i.chaoxing.com',
+ 'cnki.net', 'wanfangdata.com.cn', 'cqvip.com',
+ 'pubmed.ncbi.nlm.nih.gov', 'arxiv.org', 'researchgate.net',
+ 'academia.edu', 'ieee.org', 'acm.org', 'nature.com',
+ 'science.org', 'sciencedirect.com', 'springer.com',
+ 'wiley.com', 'tandfonline.com', 'taylorfrancis.com',
+ 'jstor.org', 'oxfordjournals.org', 'cambridge.org',
+ 'britannica.com', 'wikipedia.org', 'zh.wikipedia.org',
+ 'baike.baidu.com', 'baike.sogou.com', 'baike.qq.com',
+ 'zhihu.com', 'zhuanlan.zhihu.com',
+ 'douban.com', 'book.douban.com',
+ 'goodreads.com', 'librarything.com',
+ 'bookzz.org', 'gen.lib.rus.ec', 'sci-hub.se',
+ 'pdfdrive.com', 'z-library', 'b-ok.cc',
+ 'vocabulary.com', 'dictionary.com', 'thesaurus.com',
+ 'merriam-webster.com', 'oxforddictionaries.com',
+ 'dict.cn', 'iciba.com', 'youdao.com', 'fanyi.baidu.com',
+ 'duolingo.com', 'busuu.com', 'memrise.com',
+ 'ankiweb.net', 'quizlet.com', 'babbel.com',
+ 'rosettastone.com', 'lingualeo.com', 'polyglotclub.com'
+ ],
+ pathPatterns: [
+ '/course', '/courses', '/lesson', '/learn', '/learning',
+ '/tutorial', '/tutorials', '/guide', '/guides',
+ '/study', '/education', '/edu', '/academic',
+ '/module', '/chapter', '/unit', '/topic',
+ '/quiz', '/test', '/exam', '/assessment', '/practice',
+ '/exercise', '/homework', '/assignment', '/project',
+ '/certificate', '/certification', '/diploma', '/degree',
+ '/book', '/books', '/ebook', '/pdf', '/document',
+ '/article', '/papers', '/journal', '/publication',
+ '/research', '/study', '/thesis', '/dissertation',
+ '/library', '/archive', '/collection', '/catalog'
+ ],
+ titleKeywords: [
+ '课程', 'course', '教程', 'tutorial', '学习', 'learn',
+ '教育', 'education', '学校', 'school', '大学', 'university',
+ '学院', 'college', '课堂', 'class', '讲座', 'lecture',
+ '章节', 'chapter', '单元', 'unit', '模块', 'module',
+ '练习', 'exercise', '作业', 'homework', '测验', 'quiz',
+ '考试', 'exam', '测试', 'test', '评估', 'assessment',
+ '证书', 'certificate', '认证', 'certification', '学位',
+ '书', 'book', '电子书', 'ebook', 'PDF', '文档',
+ '文章', 'article', '论文', 'paper', '期刊', 'journal',
+ '研究', 'research', '学术', 'academic', '学位论文',
+ '图书馆', 'library', '档案馆', 'archive', '收藏',
+ '词典', 'dictionary', '词汇', 'vocabulary', '翻译',
+ '语言学习', 'language', '外语', 'foreign', '英语',
+ '单词', 'word', '语法', 'grammar', '口语', '听力',
+ '阅读', 'reading', '写作', 'writing', '翻译', 'translation'
+ ],
+ timePatterns: {
+ weekday: [19, 20, 21, 22],
+ weekend: [9, 10, 11, 14, 15, 16, 19, 20, 21]
+ }
+ },
+ weight: 1.0
+ },
+
+ entertainment: {
+ name: '娱乐',
+ icon: '🎮',
+ color: '#E64A19',
+ description: '视频游戏、社交娱乐、购物休闲',
+ patterns: {
+ domains: [
+ 'youtube.com', 'youtu.be', 'bilibili.com', 'bilibili.cn',
+ 'iqiyi.com', 'youku.com', 'tudou.com', 'mgtv.com',
+ 'le.com', 'sohu.com', 'v.qq.com', 'letv.com',
+ 'netflix.com', 'hulu.com', 'disneyplus.com', 'hbo.com',
+ 'amazon.com', 'primevideo.com', 'tiktok.com', 'douyin.com',
+ 'kuaishou.com', 'xiaohongshu.com', 'ixigua.com',
+ 'toutiao.com', '36kr.com', 'huxiu.com', 'iyiou.com',
+ 'weibo.com', 'weibo.cn', 'qzone.qq.com', 'tieba.baidu.com',
+ 'mafengwo.cn', 'douban.com', 'zhihu.com',
+ 'facebook.com', 'fb.com', 'instagram.com', 'twitter.com',
+ 'x.com', 'pinterest.com', 'tumblr.com', 'reddit.com',
+ 'snapchat.com', 'linkedin.com', 'tinder.com',
+ 'steamcommunity.com', 'store.steampowered.com',
+ 'epicgames.com', 'gog.com', 'origin.com', 'ea.com',
+ 'ubisoft.com', 'blizzard.com', 'battle.net',
+ 'riotgames.com', 'leagueoflegends.com', 'dota2.com',
+ 'csgo.com', 'minecraft.net', 'roblox.com',
+ 'twitch.tv', 'gaming.youtube.com', 'mixer.com',
+ 'douyu.com', 'huya.com', 'longzhu.com', 'zhanqi.tv',
+ 'taobao.com', 'tmall.com', 'jd.com', 'pinduoduo.com',
+ 'yangkeduo.com', 'suning.com', 'gome.com.cn',
+ 'amazon.com', 'ebay.com', 'walmart.com', 'target.com',
+ 'aliexpress.com', 'alibaba.com', '1688.com',
+ 'shein.com', 'asos.com', 'zara.com', 'hm.com',
+ 'apple.com', 'samsung.com', 'mi.com', 'xiaomi.com',
+ 'music.163.com', 'qqmusic.com', 'kugou.com',
+ 'spotify.com', 'applemusic.com', 'music.apple.com',
+ 'soundcloud.com', 'bandcamp.com', 'deezer.com',
+ 'xiami.com', 'changba.com', '5sing.kugou.com',
+ 'acfun.cn', 'dilidili.com', 'dmhy.org',
+ 'manhua.dmzj.com', 'manhua.163.com', 'manhua.qq.com',
+ 'bilibili.com', 'bilibili.cn', 'pixiv.net',
+ 'deviantart.com', 'artstation.com', 'dribbble.com',
+ 'behance.net', 'huaban.com', 'zcool.com.cn'
+ ],
+ pathPatterns: [
+ '/video', '/videos', '/watch', '/v/', '/movie', '/film',
+ '/tv', '/show', '/episode', '/season', '/drama',
+ '/anime', '/animation', '/cartoon', '/comic', '/manga',
+ '/game', '/games', '/play', '/store', '/dlc',
+ '/live', '/stream', '/broadcast', '/anchor', '/主播',
+ '/social', '/community', '/forum', '/post', '/status',
+ '/user', '/profile', '/follow', '/friend', '/message',
+ '/shop', '/store', '/product', '/item', '/goods',
+ '/cart', '/checkout', '/order', '/buy', '/purchase',
+ '/music', '/song', '/album', '/playlist', '/artist',
+ '/novel', '/book', '/story', '/chapter', '/阅读',
+ '/travel', '/trip', '/tour', '/hotel', 'flight',
+ '/food', '/restaurant', '/美食', '/recipe', 'cooking'
+ ],
+ titleKeywords: [
+ '视频', 'video', '电影', 'movie', '电视剧', 'drama',
+ '综艺', 'variety', '动漫', 'anime', '动画', 'animation',
+ '游戏', 'game', '玩', 'play', '攻略', 'guide',
+ '直播', 'live', 'stream', '主播', 'anchor',
+ '社交', 'social', '社区', 'community', '论坛', 'forum',
+ '微博', 'weibo', '朋友圈', '动态', 'post', 'status',
+ '购物', 'shopping', '商品', 'product', '订单', 'order',
+ '音乐', 'music', '歌曲', 'song', '歌单', 'playlist',
+ '小说', 'novel', '书籍', 'book', '阅读', 'read',
+ '旅行', 'travel', '旅游', 'tour', '酒店', 'hotel',
+ '机票', 'flight', '美食', 'food', '餐厅', 'restaurant',
+ '娱乐', 'entertainment', '休闲', 'leisure', '放松', 'relax'
+ ],
+ timePatterns: {
+ weekday: [12, 18, 19, 20, 21, 22, 23],
+ weekend: [9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
+ }
+ },
+ weight: 1.0
+ },
+
+ life: {
+ name: '生活',
+ icon: '🏠',
+ color: '#F57C00',
+ description: '金融理财、健康医疗、旅行生活',
+ patterns: {
+ domains: [
+ 'icbc.com.cn', 'ccb.com', 'boc.cn', 'bankcomm.com',
+ 'abchina.com', 'psbc.com', 'citicbank.com',
+ 'cmbchina.com', 'cebbank.com', 'hxb.com.cn',
+ 'cmbc.com.cn', 'cib.com.cn', 'spdb.com.cn',
+ 'pingan.com', 'citibank.com', 'hsbc.com.cn',
+ 'standardchartered.com.cn', 'dbs.com.cn',
+ 'alipay.com', ' alipay.com', 'taobao.com', 'tmall.com',
+ '95516.com', 'unionpay.com', 'chinapay.com',
+ 'weixin.qq.com', 'pay.weixin.qq.com', 'tenpay.com',
+ 'jdpay.com', 'jdfinance.com', 'lufax.com',
+ 'tdx.com.cn', 'gw.com.cn', '10jqka.com.cn',
+ 'eastmoney.com', 'xueqiu.com', 'gupiao.eastmoney.com',
+ 'hexun.com', 'jrj.com', 'stcn.com', 'cs.com.cn',
+ 'p2p.hexun.com', 'wdzj.com', 'wdzx.com.cn',
+ 'zhiping.com', 'rong360.com', 'dianrong.com',
+ 'lendingclub.com', 'prosper.com', 'kiva.org',
+ 'dxy.cn', 'haodf.com', 'guahao.com', 'jkb.com',
+ '39.net', '99.com.cn', '120.net', 'medsci.cn',
+ 'pubmed.ncbi.nlm.nih.gov', 'webmd.com', 'mayoclinic.org',
+ 'nhs.uk', 'who.int', 'cdc.gov', 'nhc.gov.cn',
+ 'ctrip.com', 'qunar.com', 'tuniu.com', 'lvmama.com',
+ 'mafengwo.cn', 'flickr.com', 'yelp.com',
+ 'tripadvisor.com', 'tripadvisor.cn', 'agoda.com',
+ 'booking.com', 'airbnb.com', 'airbnb.cn', 'expedia.com',
+ 'skyscanner.com', 'kayak.com', 'momondo.com',
+ 'meituan.com', 'dianping.com', 'nuomi.com', 'ele.me',
+ 'koubei.com', 'amap.com', 'map.baidu.com',
+ 'map.qq.com', 'map.sogou.com', 'map.baidu.com',
+ 'weather.com', 'weather.com.cn', 'moji.com',
+ 'aqistudy.cn', 'pm25.com', 'tianqi.com',
+ '58.com', 'ganji.com', 'zhilian.com', 'fang.com',
+ 'lianjia.com', 'ke.com', 'anjuke.com', 'centanet.com',
+ 'taobao.com', 'xianyu.com', 'zhuanzhuan.com',
+ 'gov.cn', '12306.cn', '12345.gov.cn',
+ 'chsi.com.cn', 'neea.edu.cn', 'cet.edu.cn',
+ 'ncre.edu.cn', 'zikao.com.cn', 'chengkao365.com',
+ 'mi.com', 'jd.com', 'suning.com', 'gome.com.cn',
+ 'car.autohome.com.cn', 'xcar.com.cn', 'yiche.com',
+ 'pcauto.com.cn', 'autohome.com.cn', 'newmotor.com.cn'
+ ],
+ pathPatterns: [
+ '/bank', '/finance', '/money', '/invest', '/investment',
+ '/stock', '/fund', '/loan', '/insurance', '/credit',
+ '/pay', '/payment', '/wallet', '/account', '/transfer',
+ '/health', '/medical', '/hospital', '/doctor', '/clinic',
+ '/medicine', '/drug', '/pharmacy', '/symptom', '/disease',
+ '/travel', '/trip', '/tour', '/vacation', '/holiday',
+ '/hotel', '/flight', '/train', '/ticket', '/booking',
+ '/map', '/location', '/address', '/route', '/navigation',
+ '/weather', '/forecast', '/climate', '/environment',
+ '/house', '/home', '/apartment', '/rent', '/buy',
+ '/car', '/vehicle', '/auto', '/maintenance', '/repair',
+ '/family', '/life', '/lifestyle', '/parenting', '/baby'
+ ],
+ titleKeywords: [
+ '银行', 'bank', '金融', 'finance', '理财', 'wealth',
+ '投资', 'invest', '股票', 'stock', '基金', 'fund',
+ '保险', 'insurance', '贷款', 'loan', '信用卡', 'credit',
+ '支付', 'pay', '付款', 'payment', '钱包', 'wallet',
+ '转账', 'transfer', '账户', 'account', '余额', 'balance',
+ '健康', 'health', '医疗', 'medical', '医院', 'hospital',
+ '医生', 'doctor', '挂号', 'appointment', '药品', 'drug',
+ '症状', 'symptom', '疾病', 'disease', '体检', 'checkup',
+ '旅行', 'travel', '旅游', 'tour', '酒店', 'hotel',
+ '机票', 'flight', '火车票', 'train', '门票', 'ticket',
+ '地图', 'map', '地址', 'address', '路线', 'route',
+ '导航', 'navigation', '天气', 'weather', '预报', 'forecast',
+ '房子', 'house', '租房', 'rent', '买房', 'buy',
+ '中介', 'agent', '房产', 'property', '房价', 'price',
+ '汽车', 'car', '车辆', 'vehicle', '保养', 'maintenance',
+ '维修', 'repair', '加油', 'gas', '停车', 'parking',
+ '生活', 'life', '家庭', 'family', '育儿', 'parenting',
+ '美食', 'food', '烹饪', 'cooking', '菜谱', 'recipe'
+ ],
+ timePatterns: {
+ weekday: [12, 18, 19, 20, 21],
+ weekend: [9, 10, 11, 14, 15, 16, 17, 18, 19]
+ }
+ },
+ weight: 1.0
+ }
+ };
+
+ console.log('🎭 SceneClassifier initialized with', Object.keys(this.scenePatterns).length, 'scenes');
+ }
+
+ async initialize() {
+ console.log('✅ SceneClassifier ready');
+ return true;
+ }
+
+ // ========== 自定义场景管理 ==========
+
+ addCustomScene(sceneName, config) {
+ const sceneKey = sceneName.toLowerCase().replace(/\s+/g, '_');
+
+ this.customScenePatterns.set(sceneKey, {
+ name: config.name || sceneName,
+ icon: config.icon || '📁',
+ color: config.color || '#607D8B',
+ description: config.description || '用户自定义场景',
+ patterns: {
+ domains: config.domains || [],
+ pathPatterns: config.pathPatterns || [],
+ titleKeywords: config.titleKeywords || []
+ },
+ weight: config.weight || 1.0,
+ isCustom: true
+ });
+
+ if (config.priority !== undefined) {
+ this.scenePriorities.set(sceneKey, config.priority);
+ }
+
+ console.log('➕ Added custom scene:', sceneName);
+ return true;
+ }
+
+ removeCustomScene(sceneName) {
+ const sceneKey = sceneName.toLowerCase().replace(/\s+/g, '_');
+ const removed = this.customScenePatterns.delete(sceneKey);
+ this.scenePriorities.delete(sceneKey);
+
+ if (removed) {
+ console.log('➖ Removed custom scene:', sceneName);
+ }
+ return removed;
+ }
+
+ setScenePriority(sceneName, priority) {
+ const sceneKey = sceneName.toLowerCase().replace(/\s+/g, '_');
+ this.scenePriorities.set(sceneKey, priority);
+ console.log('⚖️ Set priority for', sceneName, ':', priority);
+ }
+
+ setUserSceneWeight(sceneName, weight) {
+ const sceneKey = sceneName.toLowerCase().replace(/\s+/g, '_');
+ this.userSceneWeights.set(sceneKey, Math.max(0, Math.min(2, weight)));
+ console.log('📊 Set user weight for', sceneName, ':', weight);
+ }
+
+ // ========== 场景分类核心方法 ==========
+
+ classify(tab, options = {}) {
+ const url = tab.url || '';
+ const title = tab.title || '';
+ const now = new Date();
+ const hour = now.getHours();
+ const dayOfWeek = now.getDay();
+ const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
+
+ const scores = new Map();
+ const matches = [];
+
+ for (const [type, config] of Object.entries(this.scenePatterns)) {
+ const score = this.calculateSceneScore(url, title, config, hour, isWeekend);
+ const userWeight = this.userSceneWeights.get(type) || 1;
+ const finalScore = score * userWeight;
+
+ if (finalScore > 0) {
+ scores.set(type, finalScore);
+ matches.push({
+ type,
+ score: finalScore,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ description: config.description,
+ priority: this.scenePriorities.get(type) || 0
+ });
+ }
+ }
+
+ for (const [type, config] of this.customScenePatterns.entries()) {
+ const score = this.calculateSceneScore(url, title, config, hour, isWeekend);
+ const userWeight = this.userSceneWeights.get(type) || 1;
+ const finalScore = score * userWeight;
+
+ if (finalScore > 0) {
+ scores.set(type, finalScore);
+ matches.push({
+ type,
+ score: finalScore,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ description: config.description,
+ priority: this.scenePriorities.get(type) || 0,
+ isCustom: true
+ });
+ }
+ }
+
+ matches.sort((a, b) => {
+ if (Math.abs(a.score - b.score) > 0.1) {
+ return b.score - a.score;
+ }
+ return (b.priority || 0) - (a.priority || 0);
+ });
+
+ if (matches.length === 0) {
+ return {
+ type: 'other',
+ name: '其他',
+ icon: '🌐',
+ color: '#607D8B',
+ score: 0,
+ matches: []
+ };
+ }
+
+ return {
+ ...matches[0],
+ matches: matches.slice(0, 5)
+ };
+ }
+
+ calculateSceneScore(url, title, config, hour, isWeekend) {
+ const patterns = config.patterns;
+ const baseWeight = config.weight || 1;
+ let totalScore = 0;
+
+ if (!patterns) return 0;
+
+ const domainScore = this.matchDomain(url, patterns.domains || []);
+ totalScore += domainScore * 0.5;
+
+ const pathScore = this.matchPath(url, patterns.pathPatterns || []);
+ totalScore += pathScore * 0.25;
+
+ const titleScore = this.matchTitle(title, patterns.titleKeywords || []);
+ totalScore += titleScore * 0.2;
+
+ const timeScore = this.matchTime(hour, isWeekend, patterns.timePatterns);
+ totalScore += timeScore * 0.05;
+
+ return Math.min(totalScore * baseWeight, 1);
+ }
+
+ matchDomain(url, domains) {
+ if (!url || domains.length === 0) return 0;
+
+ try {
+ const urlObj = new URL(url);
+ const hostname = urlObj.hostname.toLowerCase();
+
+ for (const domain of domains) {
+ const domainLower = domain.toLowerCase();
+ if (hostname === domainLower || hostname.endsWith(`.${domainLower}`)) {
+ return 1;
+ }
+ }
+ } catch (e) {
+ const urlLower = url.toLowerCase();
+ for (const domain of domains) {
+ const domainLower = domain.toLowerCase();
+ if (urlLower.includes(domainLower)) {
+ return 0.8;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ matchPath(url, patterns) {
+ if (!url || patterns.length === 0) return 0;
+
+ try {
+ const urlObj = new URL(url);
+ const pathname = urlObj.pathname.toLowerCase();
+
+ for (const pattern of patterns) {
+ const patternLower = pattern.toLowerCase();
+ if (pathname.includes(patternLower) || pathname.startsWith(patternLower)) {
+ return 1;
+ }
+ }
+ } catch (e) {
+ const urlLower = url.toLowerCase();
+ for (const pattern of patterns) {
+ const patternLower = pattern.toLowerCase();
+ if (urlLower.includes(patternLower)) {
+ return 0.7;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ matchTitle(title, keywords) {
+ if (!title || keywords.length === 0) return 0;
+
+ const titleLower = title.toLowerCase();
+ let matchCount = 0;
+
+ for (const keyword of keywords) {
+ const keywordLower = keyword.toLowerCase();
+ if (titleLower.includes(keywordLower)) {
+ matchCount++;
+ }
+ }
+
+ if (matchCount === 0) return 0;
+ return Math.min(matchCount / Math.max(1, Math.floor(keywords.length * 0.25)), 1);
+ }
+
+ matchTime(hour, isWeekend, timePatterns) {
+ if (!timePatterns) return 0;
+
+ const targetHours = isWeekend
+ ? (timePatterns.weekend || [])
+ : (timePatterns.weekday || []);
+
+ if (targetHours.length === 0) return 0.5;
+
+ for (const targetHour of targetHours) {
+ if (hour === targetHour || (hour >= targetHour - 1 && hour <= targetHour + 1)) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ // ========== 批量分类 ==========
+
+ classifyAll(tabs) {
+ const groups = new Map();
+ const ungrouped = [];
+
+ for (const tab of tabs) {
+ const result = this.classify(tab);
+
+ if (result.score > 0.25) {
+ const key = result.type;
+ if (!groups.has(key)) {
+ groups.set(key, {
+ id: `scene_${key}`,
+ name: result.name,
+ type: 'scene',
+ sceneType: key,
+ icon: result.icon,
+ color: result.color,
+ description: result.description,
+ tabs: [],
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+ groups.get(key).tabs.push(tab);
+ } else {
+ ungrouped.push(tab);
+ }
+ }
+
+ const resultGroups = Array.from(groups.values())
+ .sort((a, b) => b.tabs.length - a.tabs.length);
+
+ if (ungrouped.length > 0) {
+ resultGroups.push({
+ id: 'scene_other',
+ name: '其他',
+ type: 'scene',
+ sceneType: 'other',
+ icon: '🌐',
+ color: '#607D8B',
+ tabs: ungrouped,
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+
+ return resultGroups;
+ }
+
+ // ========== 获取场景信息 ==========
+
+ getScenes() {
+ const scenes = [];
+
+ for (const [type, config] of Object.entries(this.scenePatterns)) {
+ scenes.push({
+ type,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ description: config.description,
+ priority: this.scenePriorities.get(type) || 0,
+ userWeight: this.userSceneWeights.get(type) || 1,
+ isCustom: false
+ });
+ }
+
+ for (const [type, config] of this.customScenePatterns.entries()) {
+ scenes.push({
+ type,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ description: config.description,
+ priority: this.scenePriorities.get(type) || 0,
+ userWeight: this.userSceneWeights.get(type) || 1,
+ isCustom: true
+ });
+ }
+
+ return scenes.sort((a, b) => (b.priority || 0) - (a.priority || 0));
+ }
+
+ getSceneInfo(type) {
+ let config = this.scenePatterns[type];
+ let isCustom = false;
+
+ if (!config) {
+ config = this.customScenePatterns.get(type);
+ isCustom = true;
+ }
+
+ if (!config) return null;
+
+ return {
+ type,
+ name: config.name,
+ icon: config.icon,
+ color: config.color,
+ description: config.description,
+ patterns: config.patterns,
+ weight: config.weight,
+ priority: this.scenePriorities.get(type) || 0,
+ userWeight: this.userSceneWeights.get(type) || 1,
+ isCustom
+ };
+ }
+
+ // ========== 持久化 ==========
+
+ async saveToStorage(storageManager) {
+ const data = {
+ customScenePatterns: Object.fromEntries(this.customScenePatterns),
+ scenePriorities: Object.fromEntries(this.scenePriorities),
+ userSceneWeights: Object.fromEntries(this.userSceneWeights)
+ };
+
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ 'tabflow:scene_config': data }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ console.log('💾 Scene config saved');
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to save scene config:', error);
+ return false;
+ }
+ }
+
+ async loadFromStorage() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get('tabflow:scene_config', (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ const data = result['tabflow:scene_config'];
+ if (data) {
+ if (data.customScenePatterns) {
+ for (const [key, value] of Object.entries(data.customScenePatterns)) {
+ this.customScenePatterns.set(key, value);
+ }
+ }
+ if (data.scenePriorities) {
+ for (const [key, value] of Object.entries(data.scenePriorities)) {
+ this.scenePriorities.set(key, value);
+ }
+ }
+ if (data.userSceneWeights) {
+ for (const [key, value] of Object.entries(data.userSceneWeights)) {
+ this.userSceneWeights.set(key, value);
+ }
+ }
+ console.log('📥 Scene config loaded');
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.warn('⚠️ Failed to load scene config:', error);
+ return false;
+ }
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = SceneClassifier;
+}
+
+if (typeof window !== 'undefined') {
+ window.SceneClassifier = SceneClassifier;
+}
diff --git a/utils/scene-grouper.js b/utils/scene-grouper.js
new file mode 100644
index 0000000..2cfc895
--- /dev/null
+++ b/utils/scene-grouper.js
@@ -0,0 +1,656 @@
+/**
+ * SceneGrouper - 场景化自动归类引擎
+ *
+ * 整合内容类型分类器和场景分类器,实现多维度自动归类:
+ * - 按网页内容类型自动归类(文档、视频、图片、办公工具)
+ * - 按访问场景自动归类(工作、娱乐、学习)
+ * - 自定义关键词匹配
+ * - 用户自定义归类权重
+ * - 手动调整分类优先级
+ */
+
+class SceneGrouper {
+ constructor(storageManager = null, eventBus = null, options = {}) {
+ this.storageManager = storageManager;
+ this.eventBus = eventBus;
+
+ this.contentTypeClassifier = options.contentTypeClassifier || new ContentTypeClassifier();
+ this.sceneClassifier = options.sceneClassifier || new SceneClassifier();
+ this.ruleEngine = options.ruleEngine || new RuleEngine();
+
+ this.enableKeywordRules = options.enableKeywordRules !== false;
+ this.enableContentTypes = options.enableContentTypes !== false;
+ this.enableScenes = options.enableScenes !== false;
+
+ this.keywordRules = new Map();
+ this.groupingModes = options.groupingModes || {
+ content: true,
+ scene: true,
+ keyword: true,
+ custom: true
+ };
+
+ const defaultPriority = options.defaultGroupingPriority || ['keyword', 'custom', 'content', 'scene'];
+ this.groupingPriorities = options.groupingPriorities || {
+ keyword: 4,
+ custom: 3,
+ content: 2,
+ scene: 1
+ };
+
+ for (let i = 0; i < defaultPriority.length; i++) {
+ const mode = defaultPriority[i];
+ if (this.groupingPriorities[mode] !== undefined) {
+ this.groupingPriorities[mode] = defaultPriority.length - i;
+ }
+ }
+
+ this.customGroupWeights = new Map();
+ this.initialized = false;
+
+ console.log('🎯 SceneGrouper initialized');
+ }
+
+ async initialize() {
+ if (this.initialized) return true;
+
+ await this.sceneClassifier.loadFromStorage();
+ await this.loadKeywordRules();
+
+ this.initialized = true;
+ console.log('✅ SceneGrouper initialized');
+ return true;
+ }
+
+ // ========== 关键词规则管理 ==========
+
+ async addKeywordRule(rule) {
+ const validation = this.validateKeywordRule(rule);
+ if (!validation.isValid) {
+ throw new Error(validation.errors.join('; '));
+ }
+
+ const ruleId = `keyword_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
+ const newRule = {
+ id: ruleId,
+ name: rule.name.trim(),
+ enabled: rule.enabled !== false,
+ priority: Number.isFinite(rule.priority) ? rule.priority : 100,
+ matchType: rule.matchType || 'contains',
+ keywords: Array.isArray(rule.keywords) ? rule.keywords : [],
+ matchField: rule.matchField || 'all',
+ caseSensitive: Boolean(rule.caseSensitive),
+ targetGroup: rule.targetGroup || rule.name,
+ weight: Number.isFinite(rule.weight) ? rule.weight : 1.0,
+ createdAt: Date.now()
+ };
+
+ this.keywordRules.set(ruleId, newRule);
+ await this.saveKeywordRules();
+
+ console.log('➕ Added keyword rule:', newRule.name);
+ return newRule;
+ }
+
+ validateKeywordRule(rule) {
+ const errors = [];
+
+ if (!rule.name || typeof rule.name !== 'string' || rule.name.trim() === '') {
+ errors.push('规则名称不能为空');
+ }
+
+ if (!rule.keywords || !Array.isArray(rule.keywords) || rule.keywords.length === 0) {
+ errors.push('关键词不能为空');
+ }
+
+ if (rule.matchType && !['contains', 'equals', 'startsWith', 'endsWith', 'regex'].includes(rule.matchType)) {
+ errors.push('matchType 必须是 contains、equals、startsWith、endsWith 或 regex');
+ }
+
+ if (rule.matchField && !['all', 'title', 'url', 'domain', 'path'].includes(rule.matchField)) {
+ errors.push('matchField 必须是 all、title、url、domain 或 path');
+ }
+
+ return { isValid: errors.length === 0, errors };
+ }
+
+ async updateKeywordRule(ruleId, updates) {
+ const rule = this.keywordRules.get(ruleId);
+ if (!rule) {
+ throw new Error('规则不存在');
+ }
+
+ const updatedRule = {
+ ...rule,
+ ...updates,
+ id: ruleId,
+ updatedAt: Date.now()
+ };
+
+ const validation = this.validateKeywordRule(updatedRule);
+ if (!validation.isValid) {
+ throw new Error(validation.errors.join('; '));
+ }
+
+ this.keywordRules.set(ruleId, updatedRule);
+ await this.saveKeywordRules();
+
+ console.log('🔄 Updated keyword rule:', updatedRule.name);
+ return updatedRule;
+ }
+
+ async deleteKeywordRule(ruleId) {
+ const deleted = this.keywordRules.delete(ruleId);
+ if (deleted) {
+ await this.saveKeywordRules();
+ console.log('➖ Deleted keyword rule:', ruleId);
+ }
+ return deleted;
+ }
+
+ async toggleKeywordRule(ruleId) {
+ const rule = this.keywordRules.get(ruleId);
+ if (!rule) return null;
+
+ rule.enabled = !rule.enabled;
+ rule.updatedAt = Date.now();
+ await this.saveKeywordRules();
+
+ console.log('🔄 Toggled keyword rule:', rule.name, '->', rule.enabled);
+ return rule;
+ }
+
+ getKeywordRules() {
+ return Array.from(this.keywordRules.values())
+ .sort((a, b) => (a.priority || 0) - (b.priority || 0));
+ }
+
+ async saveKeywordRules() {
+ try {
+ const rulesData = Object.fromEntries(this.keywordRules);
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ 'tabflow:keyword_rules': rulesData }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to save keyword rules:', error);
+ return false;
+ }
+ }
+
+ async loadKeywordRules() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get('tabflow:keyword_rules', (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ const rulesData = result['tabflow:keyword_rules'];
+ if (rulesData) {
+ for (const [key, value] of Object.entries(rulesData)) {
+ this.keywordRules.set(key, value);
+ }
+ console.log('📥 Loaded', this.keywordRules.size, 'keyword rules');
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.warn('⚠️ Failed to load keyword rules:', error);
+ return false;
+ }
+ }
+
+ // ========== 分组模式和优先级管理 ==========
+
+ setGroupingMode(mode, enabled) {
+ if (['content', 'scene', 'keyword', 'custom'].includes(mode)) {
+ this.groupingModes[mode] = enabled;
+ console.log('⚙️ Set grouping mode:', mode, '->', enabled);
+ }
+ }
+
+ setGroupingPriority(mode, priority) {
+ if (['content', 'scene', 'keyword', 'custom'].includes(mode)) {
+ this.groupingPriorities[mode] = Math.max(0, priority);
+ console.log('⚖️ Set grouping priority:', mode, '->', priority);
+ }
+ }
+
+ setCustomGroupWeight(groupName, weight) {
+ this.customGroupWeights.set(groupName, Math.max(0, Math.min(2, weight)));
+ console.log('📊 Set custom group weight:', groupName, '->', weight);
+ }
+
+ // ========== 匹配逻辑 ==========
+
+ matchKeywordRule(tab, rule) {
+ if (!rule.enabled) return null;
+
+ let targetValue = '';
+
+ switch (rule.matchField) {
+ case 'title':
+ targetValue = tab.title || '';
+ break;
+ case 'url':
+ targetValue = tab.url || '';
+ break;
+ case 'domain':
+ try {
+ targetValue = new URL(tab.url || '').hostname;
+ } catch (e) {
+ targetValue = '';
+ }
+ break;
+ case 'path':
+ try {
+ targetValue = new URL(tab.url || '').pathname;
+ } catch (e) {
+ targetValue = '';
+ }
+ break;
+ case 'all':
+ default:
+ targetValue = `${tab.title || ''} ${tab.url || ''}`;
+ break;
+ }
+
+ const searchValue = rule.caseSensitive ? targetValue : targetValue.toLowerCase();
+
+ for (const keyword of rule.keywords) {
+ const testKeyword = rule.caseSensitive ? keyword : keyword.toLowerCase();
+ let matched = false;
+
+ switch (rule.matchType) {
+ case 'contains':
+ matched = searchValue.includes(testKeyword);
+ break;
+ case 'equals':
+ matched = searchValue === testKeyword;
+ break;
+ case 'startsWith':
+ matched = searchValue.startsWith(testKeyword);
+ break;
+ case 'endsWith':
+ matched = searchValue.endsWith(testKeyword);
+ break;
+ case 'regex':
+ try {
+ const flags = rule.caseSensitive ? '' : 'i';
+ matched = new RegExp(keyword, flags).test(targetValue);
+ } catch (e) {
+ matched = false;
+ }
+ break;
+ default:
+ matched = searchValue.includes(testKeyword);
+ }
+
+ if (matched) {
+ return {
+ rule,
+ matchedKeyword: keyword,
+ score: rule.weight || 1.0
+ };
+ }
+ }
+
+ return null;
+ }
+
+ // ========== 核心分组方法 ==========
+
+ classifyTab(tab, options = {}) {
+ const results = {
+ matches: [],
+ finalGroup: null,
+ scores: {}
+ };
+
+ if (this.groupingModes.keyword) {
+ const keywordMatch = this.matchKeywordRules(tab);
+ if (keywordMatch) {
+ const priority = this.groupingPriorities.keyword + keywordMatch.score;
+ results.matches.push({
+ type: 'keyword',
+ groupName: keywordMatch.rule.targetGroup,
+ score: keywordMatch.score,
+ priority,
+ rule: keywordMatch.rule,
+ matchedKeyword: keywordMatch.matchedKeyword
+ });
+ results.scores.keyword = keywordMatch.score;
+ }
+ }
+
+ if (this.groupingModes.content) {
+ const contentResult = this.contentTypeClassifier.classify(tab);
+ if (contentResult.score > 0.3) {
+ const priority = this.groupingPriorities.content;
+ const groupWeight = this.customGroupWeights.get(contentResult.type) || 1;
+ const adjustedPriority = priority * groupWeight;
+
+ results.matches.push({
+ type: 'content',
+ groupName: contentResult.name,
+ contentType: contentResult.type,
+ score: contentResult.score,
+ priority: adjustedPriority,
+ icon: contentResult.icon,
+ color: contentResult.color
+ });
+ results.scores.content = contentResult.score;
+ }
+ }
+
+ if (this.groupingModes.scene) {
+ const sceneResult = this.sceneClassifier.classify(tab);
+ if (sceneResult.score > 0.25) {
+ const priority = this.groupingPriorities.scene;
+ const groupWeight = this.customGroupWeights.get(sceneResult.type) || 1;
+ const adjustedPriority = priority * groupWeight;
+
+ results.matches.push({
+ type: 'scene',
+ groupName: sceneResult.name,
+ sceneType: sceneResult.type,
+ score: sceneResult.score,
+ priority: adjustedPriority,
+ icon: sceneResult.icon,
+ color: sceneResult.color
+ });
+ results.scores.scene = sceneResult.score;
+ }
+ }
+
+ results.matches.sort((a, b) => {
+ if (Math.abs(a.priority - b.priority) > 0.1) {
+ return b.priority - a.priority;
+ }
+ return b.score - a.score;
+ });
+
+ if (results.matches.length > 0) {
+ results.finalGroup = results.matches[0];
+ }
+
+ return results;
+ }
+
+ matchKeywordRules(tab) {
+ const enabledRules = this.getKeywordRules().filter(r => r.enabled);
+
+ for (const rule of enabledRules) {
+ const match = this.matchKeywordRule(tab, rule);
+ if (match) {
+ return match;
+ }
+ }
+
+ return null;
+ }
+
+ // ========== 批量分组 ==========
+
+ groupTabs(tabs, options = {}) {
+ const groups = new Map();
+ const ungrouped = [];
+ const groupingMode = options.mode || 'auto';
+ const mergeThreshold = options.mergeThreshold || 0.5;
+
+ for (const tab of tabs) {
+ const classification = this.classifyTab(tab);
+
+ if (classification.finalGroup) {
+ const groupKey = this.getGroupKey(classification.finalGroup, groupingMode);
+
+ if (!groups.has(groupKey)) {
+ groups.set(groupKey, this.createGroup(classification.finalGroup));
+ }
+ groups.get(groupKey).tabs.push(tab);
+ } else {
+ ungrouped.push(tab);
+ }
+ }
+
+ const resultGroups = Array.from(groups.values())
+ .filter(g => g.tabs.length > 0)
+ .sort((a, b) => {
+ if (a.priority !== b.priority) {
+ return (b.priority || 0) - (a.priority || 0);
+ }
+ return b.tabs.length - a.tabs.length;
+ });
+
+ if (ungrouped.length > 0) {
+ resultGroups.push({
+ id: 'scene_ungrouped',
+ name: '未分类',
+ type: 'ungrouped',
+ icon: '📁',
+ color: '#607D8B',
+ tabs: ungrouped,
+ collapsed: false,
+ createdAt: Date.now(),
+ priority: 0
+ });
+ }
+
+ return resultGroups;
+ }
+
+ getGroupKey(match, groupingMode) {
+ switch (groupingMode) {
+ case 'content':
+ return `content_${match.contentType || match.groupName}`;
+ case 'scene':
+ return `scene_${match.sceneType || match.groupName}`;
+ case 'keyword':
+ return `keyword_${match.rule?.id || match.groupName}`;
+ default:
+ return `${match.type}_${match.groupName}`;
+ }
+ }
+
+ createGroup(match) {
+ return {
+ id: `group_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
+ name: match.groupName,
+ type: match.type,
+ contentType: match.contentType,
+ sceneType: match.sceneType,
+ icon: match.icon || '📁',
+ color: match.color || '#607D8B',
+ tabs: [],
+ collapsed: false,
+ createdAt: Date.now(),
+ priority: match.priority || 0,
+ rule: match.rule
+ };
+ }
+
+ // ========== 多维度联合分组 ==========
+
+ groupByMultipleDimensions(tabs, options = {}) {
+ const dimensions = options.dimensions || ['content', 'scene'];
+ const includeKeywords = options.includeKeywords !== false;
+ const minGroupSize = options.minGroupSize || 1;
+
+ let groupedTabs = new Map();
+
+ if (includeKeywords) {
+ const keywordGroups = this.groupByKeywords(tabs);
+ for (const [key, group] of keywordGroups) {
+ groupedTabs.set(key, group);
+ }
+ const matchedUuids = new Set();
+ for (const group of groupedTabs.values()) {
+ for (const tab of group.tabs) {
+ matchedUuids.add(tab.uuid);
+ }
+ }
+ tabs = tabs.filter(t => !matchedUuids.has(t.uuid));
+ }
+
+ for (const dimension of dimensions) {
+ const dimensionGroups = this.groupByDimension(tabs, dimension);
+
+ for (const [key, group] of dimensionGroups) {
+ if (group.tabs.length >= minGroupSize) {
+ const fullKey = `${dimension}_${key}`;
+ if (!groupedTabs.has(fullKey)) {
+ groupedTabs.set(fullKey, {
+ ...group,
+ type: dimension
+ });
+ }
+ }
+ }
+ }
+
+ const allMatched = new Set();
+ for (const group of groupedTabs.values()) {
+ for (const tab of group.tabs) {
+ allMatched.add(tab.uuid);
+ }
+ }
+
+ const ungrouped = tabs.filter(t => !allMatched.has(t.uuid));
+ if (ungrouped.length > 0) {
+ groupedTabs.set('ungrouped', {
+ id: 'ungrouped',
+ name: '未分类',
+ type: 'ungrouped',
+ icon: '📁',
+ color: '#607D8B',
+ tabs: ungrouped,
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+
+ return Array.from(groupedTabs.values())
+ .sort((a, b) => b.tabs.length - a.tabs.length);
+ }
+
+ groupByKeywords(tabs) {
+ const groups = new Map();
+
+ for (const tab of tabs) {
+ const match = this.matchKeywordRules(tab);
+ if (match) {
+ const groupKey = `keyword_${match.rule.id}`;
+ if (!groups.has(groupKey)) {
+ groups.set(groupKey, {
+ id: groupKey,
+ name: match.rule.targetGroup,
+ type: 'keyword',
+ icon: '🔑',
+ color: '#FF9800',
+ rule: match.rule,
+ tabs: [],
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+ groups.get(groupKey).tabs.push(tab);
+ }
+ }
+
+ return groups;
+ }
+
+ groupByDimension(tabs, dimension) {
+ const groups = new Map();
+
+ for (const tab of tabs) {
+ let result;
+
+ switch (dimension) {
+ case 'content':
+ result = this.contentTypeClassifier.classify(tab);
+ break;
+ case 'scene':
+ result = this.sceneClassifier.classify(tab);
+ break;
+ default:
+ continue;
+ }
+
+ if (result && result.score > 0.3) {
+ const key = result.type;
+ if (!groups.has(key)) {
+ groups.set(key, {
+ id: `${dimension}_${key}`,
+ name: result.name,
+ icon: result.icon,
+ color: result.color,
+ description: result.description,
+ tabs: [],
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+ groups.get(key).tabs.push(tab);
+ }
+ }
+
+ return groups;
+ }
+
+ // ========== 获取统计信息 ==========
+
+ getStats() {
+ return {
+ keywordRuleCount: this.keywordRules.size,
+ enabledKeywordRules: this.getKeywordRules().filter(r => r.enabled).length,
+ groupingModes: { ...this.groupingModes },
+ groupingPriorities: { ...this.groupingPriorities },
+ customGroupWeights: Object.fromEntries(this.customGroupWeights),
+ contentCategories: this.contentTypeClassifier.getCategories().length,
+ sceneCategories: this.sceneClassifier.getScenes().length,
+ initialized: this.initialized
+ };
+ }
+
+ // ========== 重置 ==========
+
+ async reset() {
+ this.keywordRules.clear();
+ this.customGroupWeights.clear();
+ this.groupingModes = {
+ content: true,
+ scene: true,
+ keyword: true,
+ custom: true
+ };
+ this.groupingPriorities = {
+ keyword: 4,
+ custom: 3,
+ content: 2,
+ scene: 1
+ };
+
+ await this.saveKeywordRules();
+ console.log('🔄 SceneGrouper reset');
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = SceneGrouper;
+}
+
+if (typeof window !== 'undefined') {
+ window.SceneGrouper = SceneGrouper;
+}
diff --git a/utils/smart-grouping-engine.js b/utils/smart-grouping-engine.js
new file mode 100644
index 0000000..091b828
--- /dev/null
+++ b/utils/smart-grouping-engine.js
@@ -0,0 +1,596 @@
+/**
+ * SmartGroupingEngine - 智能分组引擎
+ *
+ * 结合机器学习分类器和用户行为追踪的智能分组核心算法
+ * 提升按域名、按日期、自定义规则的分类准确率
+ */
+
+class SmartGroupingEngine {
+ constructor(storageManager = null, eventBus = null, options = {}) {
+ this.storageManager = storageManager;
+ this.eventBus = eventBus;
+ this.groupingEngine = new GroupingEngine();
+
+ this.mlClassifier = options.mlClassifier || new MLClassifier({
+ modelType: 'naive-bayes'
+ });
+
+ this.ensembleClassifier = options.ensembleClassifier || new EnsembleClassifier();
+
+ this.behaviorTracker = options.behaviorTracker;
+
+ this.useEnsemble = options.useEnsemble !== false;
+ this.enableML = options.enableML !== false;
+ this.enableBehaviorLearning = options.enableBehaviorLearning !== false;
+
+ this.useMLForDomain = options.useMLForDomain !== false;
+ this.useMLForContent = options.useMLForContent !== false;
+ this.useBehaviorData = options.useBehaviorData !== false;
+
+ this.domainGroupCache = new Map();
+ this.dateGroupCache = new Map();
+ this.customGroupCache = new Map();
+
+ this.lastGroupingHash = '';
+ this.cacheTimeout = options.cacheTimeout || 5 * 60 * 1000;
+
+ this.customWeights = options.customWeights || {
+ domain: 0.4,
+ title: 0.3,
+ urlPath: 0.2,
+ userBehavior: 0.1
+ };
+
+ console.log('🧠 SmartGroupingEngine initialized');
+ }
+
+ async initialize() {
+ console.log('✅ SmartGroupingEngine ready');
+ return true;
+ }
+
+ setBehaviorTracker(behaviorTracker) {
+ this.behaviorTracker = behaviorTracker;
+ }
+
+ setStorageManager(storageManager) {
+ this.storageManager = storageManager;
+ }
+
+ // ========== 增强的域名分组 ==========
+
+ groupByDomain(tabs, options = {}) {
+ const useML = options.useML !== false && this.enableML;
+ const useBehavior = options.useBehavior !== false && this.enableBehaviorLearning && this.behaviorTracker;
+
+ const baseGroups = this.groupingEngine.groupByDomain(tabs);
+
+ if (!useML && !useBehavior) {
+ return baseGroups;
+ }
+
+ const enhancedGroups = this.enhanceDomainGroupsWithML(baseGroups, tabs, useML, useBehavior);
+
+ return enhancedGroups;
+ }
+
+ enhanceDomainGroupsWithML(groups, tabs, useML, useBehavior) {
+ const groupMap = new Map();
+ const ungroupedTabs = [];
+
+ for (const group of groups) {
+ groupMap.set(group.id, {
+ ...group,
+ tabs: []
+ });
+ }
+
+ for (const tab of tabs) {
+ let assigned = false;
+ let bestGroup = null;
+ let bestScore = 0;
+
+ if (useML && this.mlClassifier.trained) {
+ const mlPrediction = this.predictGroupWithML(tab);
+ if (mlPrediction && mlPrediction.confidence > 0.5) {
+ for (const group of groups) {
+ if (group.name === mlPrediction.groupName ||
+ group.id.includes(mlPrediction.groupName)) {
+ const score = mlPrediction.confidence * this.customWeights.domain;
+ if (score > bestScore) {
+ bestScore = score;
+ bestGroup = group;
+ }
+ }
+ }
+ }
+ }
+
+ if (useBehavior && this.behaviorTracker) {
+ const behaviorPrediction = this.behaviorTracker.predictGroupForTab(tab);
+ if (behaviorPrediction && behaviorPrediction.confidence > 0.3) {
+ for (const group of groups) {
+ if (group.name === behaviorPrediction.groupName) {
+ const score = behaviorPrediction.confidence * this.customWeights.userBehavior;
+ if (score > bestScore) {
+ bestScore = score;
+ bestGroup = group;
+ }
+ }
+ }
+ }
+ }
+
+ if (bestGroup) {
+ const targetGroup = groupMap.get(bestGroup.id);
+ if (targetGroup) {
+ targetGroup.tabs.push(tab);
+ assigned = true;
+ }
+ }
+
+ if (!assigned) {
+ const domain = this.groupingEngine.extractDomain(tab.url);
+ if (domain) {
+ const domainGroupId = `domain_${domain.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
+ let targetGroup = groupMap.get(domainGroupId);
+
+ if (!targetGroup) {
+ targetGroup = {
+ id: domainGroupId,
+ name: domain,
+ type: 'domain',
+ tabs: [],
+ collapsed: true,
+ createdAt: Date.now(),
+ icon: this.groupingEngine.getDomainIcon(domain)
+ };
+ groupMap.set(domainGroupId, targetGroup);
+ }
+
+ targetGroup.tabs.push(tab);
+ } else {
+ ungroupedTabs.push(tab);
+ }
+ }
+ }
+
+ const resultGroups = Array.from(groupMap.values())
+ .filter(g => g.tabs.length > 0)
+ .sort((a, b) => b.tabs.length - a.tabs.length);
+
+ if (ungroupedTabs.length > 0) {
+ resultGroups.push({
+ id: 'domain_ungrouped',
+ name: 'Ungrouped',
+ type: 'domain',
+ tabs: ungroupedTabs,
+ collapsed: true,
+ createdAt: Date.now(),
+ icon: '📁'
+ });
+ }
+
+ return resultGroups;
+ }
+
+ // ========== 增强的日期分组 ==========
+
+ groupByDate(tabs, options = {}) {
+ const useBehavior = options.useBehavior !== false && this.enableBehaviorLearning && this.behaviorTracker;
+
+ const baseGroups = this.groupingEngine.groupByDate(tabs);
+
+ if (!useBehavior) {
+ return baseGroups;
+ }
+
+ return this.enhanceDateGroupsWithBehavior(baseGroups, tabs);
+ }
+
+ enhanceDateGroupsWithBehavior(groups, tabs) {
+ const now = Date.now();
+ const dayMs = 24 * 60 * 60 * 1000;
+
+ const enhancedGroups = groups.map(group => {
+ const enhancedTabs = group.tabs.map(tab => {
+ const visitScore = this.calculateVisitRecencyScore(tab, now);
+ return {
+ ...tab,
+ _visitScore: visitScore
+ };
+ });
+
+ enhancedTabs.sort((a, b) => b._visitScore - a._visitScore);
+
+ return {
+ ...group,
+ tabs: enhancedTabs.map(t => {
+ const { _visitScore, ...rest } = t;
+ return rest;
+ })
+ };
+ });
+
+ return enhancedGroups;
+ }
+
+ calculateVisitRecencyScore(tab, now) {
+ const lastAccessed = tab.lastAccessed || tab.openedAt || now;
+ const hoursSinceAccess = (now - lastAccessed) / (1000 * 60 * 60);
+
+ const decayRate = 0.01;
+ return Math.exp(-decayRate * hoursSinceAccess);
+ }
+
+ // ========== 智能自定义分组 ==========
+
+ groupBySmartRules(tabs, rules, options = {}) {
+ const useML = options.useML !== false && this.enableML;
+ const useBehavior = options.useBehavior !== false && this.enableBehaviorLearning && this.behaviorTracker;
+
+ const ruleEngine = new RuleEngine();
+ const groups = new Map();
+ const ungrouped = [];
+
+ for (const tab of tabs) {
+ let matched = false;
+ let bestRule = null;
+ let bestScore = 0;
+
+ for (const rule of rules) {
+ if (!rule.enabled) continue;
+
+ if (ruleEngine.matchesRule(tab, rule)) {
+ const score = this.calculateRuleMatchScore(tab, rule, useML, useBehavior);
+
+ if (score > bestScore) {
+ bestScore = score;
+ bestRule = rule;
+ }
+ matched = true;
+ }
+ }
+
+ if (bestRule) {
+ const groupName = bestRule.targetGroup?.name || bestRule.name;
+ if (!groups.has(groupName)) {
+ groups.set(groupName, {
+ id: `smart_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
+ name: groupName,
+ type: 'smart',
+ tabs: [],
+ collapsed: false,
+ createdAt: Date.now(),
+ ruleId: bestRule.id,
+ matchScore: bestScore
+ });
+ }
+ groups.get(groupName).tabs.push(tab);
+ } else if (!matched) {
+ ungrouped.push(tab);
+ }
+ }
+
+ const result = Array.from(groups.values());
+
+ if (ungrouped.length > 0) {
+ result.push({
+ id: 'smart_ungrouped',
+ name: '未分类',
+ type: 'smart',
+ tabs: ungrouped,
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ }
+
+ return result;
+ }
+
+ calculateRuleMatchScore(tab, rule, useML, useBehavior) {
+ let baseScore = rule.priority || 1;
+ let totalWeight = 1;
+
+ if (useML && this.mlClassifier.trained) {
+ const prediction = this.mlClassifier.predict(tab);
+ if (prediction) {
+ const groupName = rule.targetGroup?.name || rule.name;
+ if (prediction.label === groupName) {
+ baseScore += (prediction.score || 0.5) * this.customWeights.domain;
+ totalWeight += this.customWeights.domain;
+ }
+ }
+ }
+
+ if (useBehavior && this.behaviorTracker) {
+ const behaviorPrediction = this.behaviorTracker.predictGroupForTab(tab);
+ if (behaviorPrediction) {
+ const groupName = rule.targetGroup?.name || rule.name;
+ if (behaviorPrediction.groupName === groupName) {
+ baseScore += behaviorPrediction.confidence * this.customWeights.userBehavior;
+ totalWeight += this.customWeights.userBehavior;
+ }
+ }
+ }
+
+ return baseScore / totalWeight;
+ }
+
+ // ========== ML辅助预测 ==========
+
+ predictGroupWithML(tab) {
+ if (!this.mlClassifier.trained && !this.ensembleClassifier) {
+ return null;
+ }
+
+ let prediction;
+
+ if (this.useEnsemble && this.ensembleClassifier) {
+ prediction = this.ensembleClassifier.predict(tab);
+ } else {
+ prediction = this.mlClassifier.predict(tab);
+ }
+
+ if (!prediction || !prediction.label) {
+ return null;
+ }
+
+ return {
+ groupName: prediction.label,
+ confidence: prediction.score || prediction.probability || 0.5,
+ method: this.useEnsemble ? 'ensemble' : 'naive-bayes'
+ };
+ }
+
+ // ========== 训练模型 ==========
+
+ async trainFromHistory() {
+ if (!this.behaviorTracker) {
+ console.warn('⚠️ No behavior tracker available for training');
+ return false;
+ }
+
+ const trainingData = this.behaviorTracker.generateTrainingData();
+
+ if (trainingData.length === 0) {
+ console.log('ℹ️ No training data available');
+ return false;
+ }
+
+ this.mlClassifier.train(trainingData);
+
+ if (this.ensembleClassifier) {
+ this.ensembleClassifier.train(trainingData);
+ }
+
+ if (this.storageManager) {
+ await this.mlClassifier.save(this.storageManager);
+ if (this.ensembleClassifier) {
+ await this.ensembleClassifier.save(this.storageManager);
+ }
+ }
+
+ console.log('✅ Model trained with', trainingData.length, 'samples');
+ return true;
+ }
+
+ async loadModel() {
+ if (this.storageManager) {
+ await this.mlClassifier.load(this.storageManager);
+ if (this.ensembleClassifier) {
+ await this.ensembleClassifier.save(this.storageManager);
+ }
+ }
+ }
+
+ learnFromUserAction(tabs, groupName, isManual = true) {
+ if (!isManual) return;
+
+ for (const tab of tabs) {
+ this.mlClassifier.learn(tab, groupName);
+ if (this.ensembleClassifier) {
+ this.ensembleClassifier.learn(tab, groupName);
+ }
+ }
+
+ if (this.behaviorTracker) {
+ this.behaviorTracker.trackGroupingAction('manual_group', tabs, groupName, true);
+ }
+
+ console.log('📚 Learned from user action:', groupName, tabs.length, 'tabs');
+ }
+
+ // ========== 分组相似度计算 ==========
+
+ calculateTabSimilarity(tab1, tab2) {
+ let similarity = this.groupingEngine.calculateSimilarity(tab1, tab2);
+
+ const mlSimilarity = this.calculateMLSimilarity(tab1, tab2);
+ const behaviorSimilarity = this.calculateBehaviorSimilarity(tab1, tab2);
+
+ similarity = (
+ similarity * 0.5 +
+ mlSimilarity * 0.3 +
+ behaviorSimilarity * 0.2
+ );
+
+ return similarity;
+ }
+
+ calculateMLSimilarity(tab1, tab2) {
+ if (!this.mlClassifier.trained) {
+ return 0;
+ }
+
+ const pred1 = this.mlClassifier.predict(tab1);
+ const pred2 = this.mlClassifier.predict(tab2);
+
+ if (pred1 && pred2 && pred1.label === pred2.label) {
+ return Math.min((pred1.score || 0.5) + (pred2.score || 0.5), 1);
+ }
+
+ return 0;
+ }
+
+ calculateBehaviorSimilarity(tab1, tab2) {
+ if (!this.behaviorTracker) {
+ return 0;
+ }
+
+ const domain1 = this.groupingEngine.extractDomain(tab1.url);
+ const domain2 = this.groupingEngine.extractDomain(tab2.url);
+
+ if (domain1 === domain2) {
+ const group1 = this.behaviorTracker.predictGroupForTab(tab1);
+ const group2 = this.behaviorTracker.predictGroupForTab(tab2);
+
+ if (group1 && group2 && group1.groupName === group2.groupName) {
+ return (group1.confidence + group2.confidence) / 2;
+ }
+
+ return 0.5;
+ }
+
+ return 0;
+ }
+
+ // ========== 基于内容的智能分组 ==========
+
+ groupByContent(tabs, options = {}) {
+ const similarityThreshold = options.similarityThreshold || 0.6;
+ const useML = options.useML !== false && this.enableML;
+
+ const groups = [];
+ const processed = new Set();
+
+ const sortedTabs = [...tabs].sort((a, b) => {
+ const scoreA = this.calculateTabImportance(a);
+ const scoreB = this.calculateTabImportance(b);
+ return scoreB - scoreA;
+ });
+
+ for (const tab of sortedTabs) {
+ if (processed.has(tab.uuid)) continue;
+
+ const similarTabs = sortedTabs.filter(otherTab => {
+ if (processed.has(otherTab.uuid) || tab.uuid === otherTab.uuid) {
+ return false;
+ }
+
+ const similarity = this.calculateTabSimilarity(tab, otherTab);
+ return similarity >= similarityThreshold;
+ });
+
+ if (similarTabs.length > 0) {
+ const allTabs = [tab, ...similarTabs];
+ const groupName = this.generateSmartGroupName(allTabs);
+
+ groups.push({
+ id: `content_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
+ name: groupName,
+ type: 'content',
+ tabs: allTabs,
+ collapsed: false,
+ createdAt: Date.now(),
+ avgSimilarity: this.calculateGroupAverageSimilarity(allTabs)
+ });
+
+ processed.add(tab.uuid);
+ similarTabs.forEach(t => processed.add(t.uuid));
+ } else {
+ groups.push({
+ id: `single_${tab.uuid}`,
+ name: tab.title || 'Untitled',
+ type: 'single',
+ tabs: [tab],
+ collapsed: false,
+ createdAt: Date.now()
+ });
+ processed.add(tab.uuid);
+ }
+ }
+
+ return groups;
+ }
+
+ calculateTabImportance(tab) {
+ let score = 0;
+ score += (tab.visitCount || 1) * 10;
+
+ if (tab.lastAccessed) {
+ const recency = Date.now() - tab.lastAccessed;
+ score += Math.max(0, 100 - recency / (1000 * 60));
+ }
+
+ return score;
+ }
+
+ calculateGroupAverageSimilarity(tabs) {
+ if (tabs.length < 2) return 1;
+
+ let totalSimilarity = 0;
+ let pairCount = 0;
+
+ for (let i = 0; i < tabs.length; i++) {
+ for (let j = i + 1; j < tabs.length; j++) {
+ totalSimilarity += this.calculateTabSimilarity(tabs[i], tabs[j]);
+ pairCount++;
+ }
+ }
+
+ return pairCount > 0 ? totalSimilarity / pairCount : 0;
+ }
+
+ generateSmartGroupName(tabs) {
+ const mlGroups = new Map();
+ const domains = new Map();
+
+ for (const tab of tabs) {
+ const prediction = this.predictGroupWithML(tab);
+ if (prediction) {
+ mlGroups.set(prediction.groupName, (mlGroups.get(prediction.groupName) || 0) + 1);
+ }
+
+ const domain = this.groupingEngine.extractDomain(tab.url);
+ if (domain) {
+ domains.set(domain, (domains.get(domain) || 0) + 1);
+ }
+ }
+
+ if (mlGroups.size > 0) {
+ const topMLGroup = Array.from(mlGroups.entries())
+ .sort((a, b) => b[1] - a[1])[0];
+ if (topMLGroup[1] >= tabs.length * 0.5) {
+ return topMLGroup[0];
+ }
+ }
+
+ if (domains.size === 1) {
+ const domain = Array.from(domains.keys())[0];
+ return domain.replace('www.', '');
+ }
+
+ return this.groupingEngine.generateGroupName(tabs[0], tabs.slice(1));
+ }
+
+ // ========== 统计信息 ==========
+
+ getStats() {
+ return {
+ mlTrained: this.mlClassifier.trained,
+ mlStats: this.mlClassifier.getStats(),
+ behaviorTracker: this.behaviorTracker ? this.behaviorTracker.getStats() : null,
+ useEnsemble: this.useEnsemble,
+ customWeights: this.customWeights
+ };
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = SmartGroupingEngine;
+}
+
+if (typeof window !== 'undefined') {
+ window.SmartGroupingEngine = SmartGroupingEngine;
+}
diff --git a/utils/sync-manager.js b/utils/sync-manager.js
new file mode 100644
index 0000000..3ac5e3f
--- /dev/null
+++ b/utils/sync-manager.js
@@ -0,0 +1,828 @@
+/**
+ * SyncManager - 同步管理器
+ *
+ * 实现功能:
+ * - 分组规则导出/导入
+ * - 多设备同步
+ * - 跨窗口分组联动
+ * - 冲突解决
+ * - 增量同步
+ */
+
+class SyncManager {
+ constructor(storageManager, eventBus, options = {}) {
+ this.storageManager = storageManager;
+ this.eventBus = eventBus;
+
+ this.syncStorageKey = 'tabflow:sync_data';
+ this.lastSyncKey = 'tabflow:last_sync';
+ this.conflictsKey = 'tabflow:sync_conflicts';
+
+ this.syncInterval = options.syncInterval || 30 * 1000;
+ this.maxSyncRetries = options.maxSyncRetries || 3;
+ this.conflictResolutionStrategy = options.conflictResolutionStrategy || 'latest-wins';
+
+ this.isSyncing = false;
+ this.syncTimer = null;
+ this.pendingChanges = [];
+ this.conflicts = [];
+
+ this.deviceId = this.generateDeviceId();
+ this.sessionId = this.generateSessionId();
+
+ this.listeners = {
+ syncStarted: [],
+ syncCompleted: [],
+ syncFailed: [],
+ conflictDetected: [],
+ conflictResolved: [],
+ dataExported: [],
+ dataImported: []
+ };
+
+ this.initialized = false;
+
+ console.log('🔄 SyncManager initialized, deviceId:', this.deviceId);
+ }
+
+ generateDeviceId() {
+ const stored = localStorage.getItem('tabflow:device_id');
+ if (stored) return stored;
+
+ const newId = `device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ localStorage.setItem('tabflow:device_id', newId);
+ return newId;
+ }
+
+ generateSessionId() {
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
+ }
+
+ // ========== 初始化 ==========
+
+ async initialize() {
+ if (this.initialized) return true;
+
+ await this.loadConflicts();
+ this.setupEventListeners();
+ this.startAutoSync();
+
+ this.initialized = true;
+ console.log('✅ SyncManager initialized');
+ return true;
+ }
+
+ setupEventListeners() {
+ if (!this.eventBus) return;
+
+ this.eventBus.on('group_created', (group) => {
+ this.recordChange('create', 'group', group.id, group);
+ });
+
+ this.eventBus.on('group_updated', ({ group }) => {
+ this.recordChange('update', 'group', group.id, group);
+ });
+
+ this.eventBus.on('group_deleted', ({ groupId }) => {
+ this.recordChange('delete', 'group', groupId);
+ });
+
+ this.eventBus.on('tab_moved_to_group', ({ tab, groupId }) => {
+ this.recordChange('update', 'tab', tab.uuid || tab.id, {
+ ...tab,
+ groupId,
+ updatedAt: Date.now()
+ });
+ });
+
+ this.eventBus.on('tab_note_updated', ({ tab, note }) => {
+ this.recordChange('update', 'tab', tab.uuid || tab.id, {
+ ...tab,
+ note,
+ updatedAt: Date.now()
+ });
+ });
+
+ this.eventBus.on('tab_alias_updated', ({ tab, alias }) => {
+ this.recordChange('update', 'tab', tab.uuid || tab.id, {
+ ...tab,
+ alias,
+ updatedAt: Date.now()
+ });
+ });
+ }
+
+ // ========== 变更记录 ==========
+
+ recordChange(operation, entityType, entityId, data = null) {
+ const change = {
+ id: `change_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
+ operation,
+ entityType,
+ entityId,
+ data,
+ deviceId: this.deviceId,
+ sessionId: this.sessionId,
+ timestamp: Date.now(),
+ synced: false
+ };
+
+ this.pendingChanges.push(change);
+
+ if (this.pendingChanges.length > 100) {
+ this.pendingChanges = this.pendingChanges.slice(-50);
+ }
+
+ console.log('📝 Change recorded:', operation, entityType, entityId);
+ }
+
+ // ========== 自动同步 ==========
+
+ startAutoSync() {
+ if (this.syncTimer) {
+ clearInterval(this.syncTimer);
+ }
+
+ this.syncTimer = setInterval(() => {
+ this.performSync().catch(error => {
+ console.warn('⚠️ Auto-sync failed:', error);
+ });
+ }, this.syncInterval);
+
+ console.log('⏰ Auto-sync started, interval:', this.syncInterval, 'ms');
+ }
+
+ stopAutoSync() {
+ if (this.syncTimer) {
+ clearInterval(this.syncTimer);
+ this.syncTimer = null;
+ console.log('⏹️ Auto-sync stopped');
+ }
+ }
+
+ async performSync(forceFull = false) {
+ if (this.isSyncing) {
+ console.log('⏳ Sync already in progress, skipping');
+ return false;
+ }
+
+ this.isSyncing = true;
+ this.notify('syncStarted', { timestamp: Date.now(), forceFull });
+
+ try {
+ const lastSync = await this.getLastSyncTime();
+ const syncData = await this.collectSyncData(forceFull, lastSync);
+
+ if (!forceFull && syncData.changes.length === 0 && syncData.removals.length === 0) {
+ console.log('ℹ️ No changes to sync');
+ this.notify('syncCompleted', {
+ timestamp: Date.now(),
+ changesCount: 0,
+ wasFull: forceFull
+ });
+ return true;
+ }
+
+ const conflicts = await this.detectConflicts(syncData);
+
+ if (conflicts.length > 0) {
+ console.log('⚔️ Detected', conflicts.length, 'conflicts during sync');
+
+ for (const conflict of conflicts) {
+ const resolved = await this.resolveConflict(conflict);
+ if (!resolved) {
+ await this.addConflict(conflict);
+ }
+ }
+ }
+
+ await this.applySync(syncData);
+ await this.setLastSyncTime(Date.now());
+
+ this.pendingChanges = this.pendingChanges.filter(c => c.synced);
+
+ console.log('✅ Sync completed');
+ this.notify('syncCompleted', {
+ timestamp: Date.now(),
+ changesCount: syncData.changes.length + syncData.removals.length,
+ wasFull: forceFull
+ });
+
+ return true;
+
+ } catch (error) {
+ console.error('❌ Sync failed:', error);
+ this.notify('syncFailed', {
+ timestamp: Date.now(),
+ error: error.message
+ });
+ throw error;
+
+ } finally {
+ this.isSyncing = false;
+ }
+ }
+
+ async collectSyncData(forceFull, lastSyncTime) {
+ const syncData = {
+ changes: [],
+ removals: [],
+ metadata: {
+ deviceId: this.deviceId,
+ sessionId: this.sessionId,
+ timestamp: Date.now(),
+ lastSyncTime,
+ isFullSync: forceFull
+ }
+ };
+
+ if (forceFull) {
+ const allGroups = await this.storageManager.getAllGroups();
+ const allTabs = await this.storageManager.getAllTabs();
+
+ for (const [groupId, group] of Object.entries(allGroups)) {
+ syncData.changes.push({
+ operation: 'create',
+ entityType: 'group',
+ entityId: groupId,
+ data: group,
+ timestamp: group.createdAt || group.updatedAt || Date.now()
+ });
+ }
+
+ for (const [tabId, tab] of Object.entries(allTabs)) {
+ syncData.changes.push({
+ operation: 'create',
+ entityType: 'tab',
+ entityId: tabId,
+ data: tab,
+ timestamp: tab.lastAccessed || tab.openedAt || Date.now()
+ });
+ }
+ } else {
+ const unsyncedChanges = this.pendingChanges.filter(c => !c.synced);
+
+ for (const change of unsyncedChanges) {
+ if (change.timestamp > lastSyncTime) {
+ if (change.operation === 'delete') {
+ syncData.removals.push({
+ entityType: change.entityType,
+ entityId: change.entityId,
+ timestamp: change.timestamp
+ });
+ } else {
+ syncData.changes.push({
+ operation: change.operation,
+ entityType: change.entityType,
+ entityId: change.entityId,
+ data: change.data,
+ timestamp: change.timestamp
+ });
+ }
+ }
+ }
+ }
+
+ return syncData;
+ }
+
+ async detectConflicts(syncData) {
+ const conflicts = [];
+ const remoteData = await this.getRemoteSyncData();
+
+ if (!remoteData) return conflicts;
+
+ const localChanges = new Map();
+ for (const change of syncData.changes) {
+ const key = `${change.entityType}:${change.entityId}`;
+ localChanges.set(key, change);
+ }
+
+ const remoteChanges = new Map();
+ for (const change of (remoteData.changes || [])) {
+ const key = `${change.entityType}:${change.entityId}`;
+ remoteChanges.set(key, change);
+ }
+
+ for (const [key, localChange] of localChanges.entries()) {
+ const remoteChange = remoteChanges.get(key);
+
+ if (remoteChange && remoteChange.timestamp > localChange.timestamp) {
+ conflicts.push({
+ key,
+ entityType: localChange.entityType,
+ entityId: localChange.entityId,
+ localChange,
+ remoteChange,
+ detectedAt: Date.now()
+ });
+ }
+ }
+
+ return conflicts;
+ }
+
+ async resolveConflict(conflict) {
+ let resolved = false;
+
+ switch (this.conflictResolutionStrategy) {
+ case 'latest-wins':
+ if (conflict.remoteChange.timestamp > conflict.localChange.timestamp) {
+ await this.applyRemoteChange(conflict.remoteChange);
+ resolved = true;
+ } else {
+ await this.applyLocalChange(conflict.localChange);
+ resolved = true;
+ }
+ break;
+
+ case 'local-wins':
+ await this.applyLocalChange(conflict.localChange);
+ resolved = true;
+ break;
+
+ case 'remote-wins':
+ await this.applyRemoteChange(conflict.remoteChange);
+ resolved = true;
+ break;
+
+ case 'merge':
+ await this.mergeChanges(conflict.localChange, conflict.remoteChange);
+ resolved = true;
+ break;
+
+ default:
+ resolved = false;
+ }
+
+ if (resolved) {
+ this.notify('conflictResolved', {
+ conflict,
+ strategy: this.conflictResolutionStrategy
+ });
+ }
+
+ return resolved;
+ }
+
+ async applyLocalChange(change) {
+ if (change.entityType === 'group') {
+ if (change.operation === 'delete') {
+ await this.storageManager.removeGroup(change.entityId);
+ } else {
+ await this.storageManager.saveGroup(change.data);
+ }
+ } else if (change.entityType === 'tab') {
+ if (change.operation === 'delete') {
+ await this.storageManager.removeTab(change.entityId);
+ } else {
+ await this.storageManager.saveTab(change.data);
+ }
+ }
+ }
+
+ async applyRemoteChange(change) {
+ return this.applyLocalChange(change);
+ }
+
+ async mergeChanges(localChange, remoteChange) {
+ if (!localChange.data || !remoteChange.data) {
+ return this.applyLocalChange(localChange);
+ }
+
+ const mergedData = {
+ ...localChange.data,
+ ...remoteChange.data
+ };
+
+ if (localChange.timestamp > remoteChange.timestamp) {
+ mergedData = { ...remoteChange.data, ...localChange.data };
+ }
+
+ await this.applyLocalChange({
+ ...localChange,
+ data: mergedData
+ });
+ }
+
+ async applySync(syncData) {
+ for (const change of syncData.changes) {
+ await this.applyLocalChange(change);
+
+ const pending = this.pendingChanges.find(
+ c => c.entityId === change.entityId && c.entityType === change.entityType
+ );
+ if (pending) {
+ pending.synced = true;
+ }
+ }
+
+ for (const removal of syncData.removals) {
+ if (removal.entityType === 'group') {
+ await this.storageManager.removeGroup(removal.entityId);
+ } else if (removal.entityType === 'tab') {
+ await this.storageManager.removeTab(removal.entityId);
+ }
+ }
+ }
+
+ // ========== 远程数据存储(使用 chrome.storage.sync) ==========
+
+ async getRemoteSyncData() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.sync.get(this.syncStorageKey, (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ return result[this.syncStorageKey] || null;
+ } catch (error) {
+ console.warn('⚠️ Failed to get remote sync data:', error);
+ return null;
+ }
+ }
+
+ async setRemoteSyncData(syncData) {
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.sync.set({ [this.syncStorageKey]: syncData }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to set remote sync data:', error);
+ return false;
+ }
+ }
+
+ async getLastSyncTime() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get(this.lastSyncKey, (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ return result[this.lastSyncKey] || 0;
+ } catch (error) {
+ console.warn('⚠️ Failed to get last sync time:', error);
+ return 0;
+ }
+ }
+
+ async setLastSyncTime(timestamp) {
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ [this.lastSyncKey]: timestamp }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to set last sync time:', error);
+ return false;
+ }
+ }
+
+ // ========== 冲突管理 ==========
+
+ async addConflict(conflict) {
+ this.conflicts.push({
+ ...conflict,
+ addedAt: Date.now(),
+ resolved: false
+ });
+ await this.saveConflicts();
+
+ this.notify('conflictDetected', { conflict });
+ }
+
+ async resolveConflictManually(conflictId, choice) {
+ const conflictIndex = this.conflicts.findIndex(c => c.id === conflictId);
+ if (conflictIndex === -1) return false;
+
+ const conflict = this.conflicts[conflictIndex];
+
+ if (choice === 'local') {
+ await this.applyLocalChange(conflict.localChange);
+ } else if (choice === 'remote') {
+ await this.applyRemoteChange(conflict.remoteChange);
+ } else if (choice === 'merge') {
+ await this.mergeChanges(conflict.localChange, conflict.remoteChange);
+ }
+
+ conflict.resolved = true;
+ conflict.resolvedAt = Date.now();
+ conflict.resolution = choice;
+
+ await this.saveConflicts();
+ this.notify('conflictResolved', { conflict, choice });
+
+ return true;
+ }
+
+ getUnresolvedConflicts() {
+ return this.conflicts.filter(c => !c.resolved);
+ }
+
+ getAllConflicts() {
+ return [...this.conflicts];
+ }
+
+ async saveConflicts() {
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ [this.conflictsKey]: this.conflicts }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ console.error('❌ Failed to save conflicts:', error);
+ }
+ }
+
+ async loadConflicts() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get(this.conflictsKey, (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ this.conflicts = result[this.conflictsKey] || [];
+ } catch (error) {
+ console.warn('⚠️ Failed to load conflicts:', error);
+ this.conflicts = [];
+ }
+ }
+
+ // ========== 导出/导入功能 ==========
+
+ async exportData(options = {}) {
+ const exportData = {
+ version: '1.0',
+ exportedAt: new Date().toISOString(),
+ exportedBy: this.deviceId,
+ groups: {},
+ tabs: {},
+ rules: {},
+ settings: null,
+ metadata: {
+ includeGroups: options.includeGroups !== false,
+ includeTabs: options.includeTabs !== false,
+ includeRules: options.includeRules !== false,
+ includeSettings: options.includeSettings === true
+ }
+ };
+
+ if (options.includeGroups !== false) {
+ const groups = await this.storageManager.getAllGroups();
+ exportData.groups = groups;
+ }
+
+ if (options.includeTabs !== false) {
+ const tabs = await this.storageManager.getAllTabs();
+ exportData.tabs = tabs;
+ }
+
+ if (options.includeRules !== false) {
+ const rules = await this.getGroupingRules();
+ exportData.rules = rules;
+ }
+
+ if (options.includeSettings === true) {
+ const settings = await this.storageManager.getSettings();
+ exportData.settings = settings;
+ }
+
+ const jsonString = JSON.stringify(exportData, null, 2);
+ this.notify('dataExported', { exportData });
+
+ return jsonString;
+ }
+
+ async importData(jsonString, options = {}) {
+ let importData;
+ try {
+ importData = JSON.parse(jsonString);
+ } catch (error) {
+ throw new Error(`Invalid JSON format: ${error.message}`);
+ }
+
+ const validation = this.validateImportData(importData);
+ if (!validation.isValid) {
+ throw new Error(`Validation failed: ${validation.errors.join('; ')}`);
+ }
+
+ if (options.clearExisting === true) {
+ await this.storageManager.clearAllData();
+ }
+
+ let importedCount = 0;
+
+ if (importData.groups && Object.keys(importData.groups).length > 0) {
+ for (const [groupId, group] of Object.entries(importData.groups)) {
+ if (options.mergeStrategy === 'skip') {
+ const existing = await this.storageManager.getGroup(groupId);
+ if (existing) continue;
+ }
+ await this.storageManager.saveGroup(group);
+ importedCount++;
+ }
+ }
+
+ if (importData.tabs && Object.keys(importData.tabs).length > 0) {
+ for (const [tabId, tab] of Object.entries(importData.tabs)) {
+ await this.storageManager.saveTab(tab);
+ importedCount++;
+ }
+ }
+
+ if (importData.rules && Object.keys(importData.rules).length > 0) {
+ await this.saveGroupingRules(importData.rules);
+ importedCount += Object.keys(importData.rules).length;
+ }
+
+ this.notify('dataImported', {
+ importData,
+ importedCount,
+ options
+ });
+
+ return {
+ success: true,
+ importedCount,
+ groupsCount: Object.keys(importData.groups || {}).length,
+ tabsCount: Object.keys(importData.tabs || {}).length,
+ rulesCount: Object.keys(importData.rules || {}).length
+ };
+ }
+
+ validateImportData(data) {
+ const errors = [];
+
+ if (!data.version) {
+ errors.push('Missing version field');
+ }
+
+ if (!data.exportedAt) {
+ errors.push('Missing exportedAt field');
+ }
+
+ if (data.groups && typeof data.groups !== 'object') {
+ errors.push('Invalid groups format');
+ }
+
+ if (data.tabs && typeof data.tabs !== 'object') {
+ errors.push('Invalid tabs format');
+ }
+
+ if (data.rules && typeof data.rules !== 'object') {
+ errors.push('Invalid rules format');
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors
+ };
+ }
+
+ // ========== 规则导入导出辅助方法 ==========
+
+ async getGroupingRules() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get('tabflow:grouping_rules', (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ return result['tabflow:grouping_rules'] || {};
+ } catch (error) {
+ console.warn('⚠️ Failed to get grouping rules:', error);
+ return {};
+ }
+ }
+
+ async saveGroupingRules(rules) {
+ try {
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({ 'tabflow:grouping_rules': rules }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to save grouping rules:', error);
+ return false;
+ }
+ }
+
+ // ========== 监听系统 ==========
+
+ on(event, callback) {
+ if (this.listeners[event]) {
+ this.listeners[event].push(callback);
+ }
+ }
+
+ off(event, callback) {
+ if (this.listeners[event]) {
+ const index = this.listeners[event].indexOf(callback);
+ if (index > -1) {
+ this.listeners[event].splice(index, 1);
+ }
+ }
+ }
+
+ notify(event, data) {
+ if (this.listeners[event]) {
+ for (const callback of this.listeners[event]) {
+ try {
+ callback(data);
+ } catch (error) {
+ console.error(`❌ Error in ${event} listener:`, error);
+ }
+ }
+ }
+ }
+
+ // ========== 配置 ==========
+
+ setConflictResolutionStrategy(strategy) {
+ const validStrategies = ['latest-wins', 'local-wins', 'remote-wins', 'merge', 'manual'];
+ if (!validStrategies.includes(strategy)) {
+ throw new Error(`Invalid strategy: ${strategy}. Must be one of: ${validStrategies.join(', ')}`);
+ }
+ this.conflictResolutionStrategy = strategy;
+ console.log('⚙️ Conflict resolution strategy set to:', strategy);
+ }
+
+ setSyncInterval(intervalMs) {
+ this.syncInterval = intervalMs;
+ if (this.syncTimer) {
+ this.stopAutoSync();
+ this.startAutoSync();
+ }
+ console.log('⚙️ Sync interval set to:', intervalMs, 'ms');
+ }
+
+ // ========== 统计信息 ==========
+
+ getStats() {
+ return {
+ deviceId: this.deviceId,
+ sessionId: this.sessionId,
+ initialized: this.initialized,
+ isSyncing: this.isSyncing,
+ pendingChangesCount: this.pendingChanges.length,
+ unsyncedChangesCount: this.pendingChanges.filter(c => !c.synced).length,
+ conflictsCount: this.conflicts.length,
+ unresolvedConflictsCount: this.getUnresolvedConflicts().length,
+ lastSyncTime: this.getLastSyncTime(),
+ syncInterval: this.syncInterval,
+ conflictResolutionStrategy: this.conflictResolutionStrategy
+ };
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = SyncManager;
+}
+
+if (typeof window !== 'undefined') {
+ window.SyncManager = SyncManager;
+}
diff --git a/utils/user-behavior-tracker.js b/utils/user-behavior-tracker.js
new file mode 100644
index 0000000..bd606ea
--- /dev/null
+++ b/utils/user-behavior-tracker.js
@@ -0,0 +1,475 @@
+/**
+ * UserBehaviorTracker - 用户行为追踪器
+ *
+ * 追踪用户使用习惯,包括:
+ * - 历史分组操作记录
+ * - 标签页访问频率
+ * - 标签页停留时长
+ * - 用户偏好学习
+ */
+
+class UserBehaviorTracker {
+ constructor(storageManager, eventBusOrOptions = null, options = {}) {
+ this.storageManager = storageManager;
+
+ // 检测第二个参数是 eventBus 还是 options (向后兼容)
+ if (eventBusOrOptions && typeof eventBusOrOptions.on === 'function' && typeof eventBusOrOptions.emit === 'function') {
+ this.eventBus = eventBusOrOptions;
+ this.options = options;
+ } else {
+ this.eventBus = null;
+ this.options = eventBusOrOptions || {};
+ }
+
+ this.storageKey = this.options.storageKey || 'tabflow:user_behavior';
+ this.preferencesKey = this.options.preferencesKey || 'tabflow:user_preferences';
+
+ this.maxHistorySize = this.options.maxHistorySize || 1000;
+ this.maxVisitHistory = this.options.maxVisitHistory || 500;
+ this.decayRate = this.options.decayRate || 0.001;
+
+ this.groupingHistory = [];
+ this.visitHistory = [];
+ this.domainVisitCounts = new Map();
+ this.domainTotalTime = new Map();
+ this.manualGroupPreferences = new Map();
+ this.contentTypePreferences = new Map();
+
+ this.sessionStartTime = null;
+ this.currentActiveTab = null;
+ this.tabSwitchTimes = new Map();
+
+ this.initialized = false;
+
+ console.log('📊 UserBehaviorTracker initialized');
+ }
+
+ async initialize() {
+ if (this.initialized) {
+ return true;
+ }
+
+ try {
+ await this.loadFromStorage();
+ this.sessionStartTime = Date.now();
+ this.initialized = true;
+
+ console.log('✅ UserBehaviorTracker initialized');
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to initialize UserBehaviorTracker:', error);
+ return false;
+ }
+ }
+
+ async loadFromStorage() {
+ try {
+ const result = await new Promise((resolve, reject) => {
+ chrome.storage.local.get([this.storageKey, this.preferencesKey], (data) => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(data);
+ }
+ });
+ });
+
+ const behaviorData = result[this.storageKey];
+ const preferencesData = result[this.preferencesKey];
+
+ if (behaviorData) {
+ this.groupingHistory = behaviorData.groupingHistory || [];
+ this.visitHistory = behaviorData.visitHistory || [];
+ this.domainVisitCounts = new Map(Object.entries(behaviorData.domainVisitCounts || {}));
+ this.domainTotalTime = new Map(Object.entries(behaviorData.domainTotalTime || {}));
+ }
+
+ if (preferencesData) {
+ this.manualGroupPreferences = new Map(Object.entries(preferencesData.manualGroupPreferences || {}));
+ this.contentTypePreferences = new Map(Object.entries(preferencesData.contentTypePreferences || {}));
+ }
+
+ console.log('📥 User behavior data loaded');
+ return true;
+ } catch (error) {
+ console.warn('⚠️ Failed to load user behavior data:', error);
+ return false;
+ }
+ }
+
+ async saveToStorage() {
+ try {
+ const behaviorData = {
+ groupingHistory: this.groupingHistory,
+ visitHistory: this.visitHistory.slice(-this.maxVisitHistory),
+ domainVisitCounts: Object.fromEntries(this.domainVisitCounts),
+ domainTotalTime: Object.fromEntries(this.domainTotalTime),
+ lastUpdated: Date.now()
+ };
+
+ const preferencesData = {
+ manualGroupPreferences: Object.fromEntries(this.manualGroupPreferences),
+ contentTypePreferences: Object.fromEntries(this.contentTypePreferences),
+ lastUpdated: Date.now()
+ };
+
+ await new Promise((resolve, reject) => {
+ chrome.storage.local.set({
+ [this.storageKey]: behaviorData,
+ [this.preferencesKey]: preferencesData
+ }, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+
+ console.log('💾 User behavior data saved');
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to save user behavior data:', error);
+ return false;
+ }
+ }
+
+ // ========== 标签页访问追踪 ==========
+
+ trackTabActivated(tab) {
+ const now = Date.now();
+
+ if (this.currentActiveTab) {
+ const previousTabId = this.currentActiveTab.id;
+ const startTime = this.tabSwitchTimes.get(previousTabId) || now;
+ const duration = now - startTime;
+
+ if (duration > 0) {
+ this.addVisitRecord(this.currentActiveTab, duration);
+ }
+ }
+
+ this.currentActiveTab = tab;
+ this.tabSwitchTimes.set(tab.id, now);
+
+ const domain = this.extractDomain(tab.url);
+ if (domain) {
+ const currentCount = this.domainVisitCounts.get(domain) || 0;
+ this.domainVisitCounts.set(domain, currentCount + 1);
+ }
+
+ console.log('📌 Tab tracked:', tab.title, '->', domain);
+ }
+
+ addVisitRecord(tab, duration) {
+ const domain = this.extractDomain(tab.url);
+ if (!domain) return;
+
+ const record = {
+ tabId: tab.id,
+ url: tab.url,
+ title: tab.title,
+ domain,
+ startTime: this.tabSwitchTimes.get(tab.id) || Date.now(),
+ endTime: Date.now(),
+ duration,
+ timestamp: Date.now()
+ };
+
+ this.visitHistory.push(record);
+ if (this.visitHistory.length > this.maxVisitHistory) {
+ this.visitHistory.shift();
+ }
+
+ const currentTotal = this.domainTotalTime.get(domain) || 0;
+ this.domainTotalTime.set(domain, currentTotal + duration);
+ }
+
+ trackTabCreated(tab) {
+ const domain = this.extractDomain(tab.url);
+ if (domain) {
+ const currentCount = this.domainVisitCounts.get(domain) || 0;
+ this.domainVisitCounts.set(domain, currentCount + 1);
+ }
+ }
+
+ trackTabClosed(tab) {
+ if (this.currentActiveTab && this.currentActiveTab.id === tab.id) {
+ const startTime = this.tabSwitchTimes.get(tab.id);
+ if (startTime) {
+ const duration = Date.now() - startTime;
+ this.addVisitRecord(tab, duration);
+ }
+ this.currentActiveTab = null;
+ this.tabSwitchTimes.delete(tab.id);
+ }
+ }
+
+ // ========== 分组操作追踪 ==========
+
+ trackGroupingAction(actionType, tabs, groupName, isManual = false) {
+ const record = {
+ actionType,
+ groupName,
+ isManual,
+ tabs: tabs.map(tab => ({
+ id: tab.id,
+ url: tab.url,
+ title: tab.title,
+ domain: this.extractDomain(tab.url)
+ })),
+ timestamp: Date.now()
+ };
+
+ this.groupingHistory.push(record);
+ if (this.groupingHistory.length > this.maxHistorySize) {
+ this.groupingHistory.shift();
+ }
+
+ if (isManual) {
+ this.learnManualPreference(tabs, groupName);
+ }
+
+ console.log('📊 Grouping action tracked:', actionType, groupName, isManual ? '(manual)' : '(auto)');
+ }
+
+ learnManualPreference(tabs, groupName) {
+ for (const tab of tabs) {
+ const domain = this.extractDomain(tab.url);
+ if (!domain) continue;
+
+ const key = `${domain}:${groupName}`;
+ const currentCount = this.manualGroupPreferences.get(key) || 0;
+ this.manualGroupPreferences.set(key, currentCount + 1);
+
+ const domainKey = `domain:${domain}`;
+ const domainCount = this.manualGroupPreferences.get(domainKey) || 0;
+ this.manualGroupPreferences.set(domainKey, domainCount + 1);
+ }
+ }
+
+ // ========== 内容类型和场景偏好学习 ==========
+
+ trackContentPreference(tab, contentType, scene) {
+ const domain = this.extractDomain(tab.url);
+ if (!domain) return;
+
+ const contentKey = `content:${domain}:${contentType}`;
+ const contentCount = this.contentTypePreferences.get(contentKey) || 0;
+ this.contentTypePreferences.set(contentKey, contentCount + 1);
+
+ if (scene) {
+ const sceneKey = `scene:${domain}:${scene}`;
+ const sceneCount = this.contentTypePreferences.get(sceneKey) || 0;
+ this.contentTypePreferences.set(sceneKey, sceneCount + 1);
+ }
+ }
+
+ // ========== 行为分析和预测 ==========
+
+ analyzeDomainPreferences() {
+ const preferences = [];
+ const now = Date.now();
+
+ for (const [domain, count] of this.domainVisitCounts.entries()) {
+ const totalTime = this.domainTotalTime.get(domain) || 0;
+ const avgTime = count > 0 ? totalTime / count : 0;
+
+ const recencyScore = this.calculateRecencyScore(domain);
+ const frequencyScore = Math.min(count / 10, 1);
+ const durationScore = Math.min(avgTime / (5 * 60 * 1000), 1);
+
+ const combinedScore = (
+ recencyScore * 0.4 +
+ frequencyScore * 0.35 +
+ durationScore * 0.25
+ );
+
+ preferences.push({
+ domain,
+ visitCount: count,
+ totalTime,
+ avgTime,
+ recencyScore,
+ frequencyScore,
+ durationScore,
+ preferenceScore: combinedScore
+ });
+ }
+
+ return preferences.sort((a, b) => b.preferenceScore - a.preferenceScore);
+ }
+
+ calculateRecencyScore(domain) {
+ const recentVisits = this.visitHistory
+ .filter(record => record.domain === domain)
+ .slice(-5);
+
+ if (recentVisits.length === 0) return 0;
+
+ const now = Date.now();
+ const lastVisit = recentVisits[recentVisits.length - 1];
+ const hoursSinceLastVisit = (now - lastVisit.timestamp) / (1000 * 60 * 60);
+
+ return Math.exp(-this.decayRate * hoursSinceLastVisit);
+ }
+
+ predictGroupForTab(tab) {
+ const domain = this.extractDomain(tab.url);
+ if (!domain) return null;
+
+ const candidates = [];
+
+ for (const [key, count] of this.manualGroupPreferences.entries()) {
+ if (key.startsWith(`${domain}:`)) {
+ const groupName = key.split(':')[1];
+ const totalDomainCount = this.manualGroupPreferences.get(`domain:${domain}`) || 1;
+ const confidence = count / totalDomainCount;
+
+ candidates.push({
+ groupName,
+ count,
+ confidence
+ });
+ }
+ }
+
+ if (candidates.length === 0) {
+ return null;
+ }
+
+ candidates.sort((a, b) => b.confidence - a.confidence);
+ return candidates[0];
+ }
+
+ getTopDomains(limit = 10) {
+ const preferences = this.analyzeDomainPreferences();
+ return preferences.slice(0, limit);
+ }
+
+ getRecommendedGroups(tab) {
+ const domain = this.extractDomain(tab.url);
+ if (!domain) return [];
+
+ const recommendations = [];
+
+ for (const [key, count] of this.manualGroupPreferences.entries()) {
+ if (key.startsWith(`${domain}:`)) {
+ const groupName = key.split(':')[1];
+ recommendations.push({
+ groupName,
+ count,
+ score: count
+ });
+ }
+ }
+
+ return recommendations.sort((a, b) => b.count - a.count);
+ }
+
+ // ========== 时间衰减的历史数据清理 ==========
+
+ applyTimeDecay(halfLifeDays = 7) {
+ const now = Date.now();
+ const halfLifeMs = halfLifeDays * 24 * 60 * 60 * 1000;
+ const decayFactor = Math.log(2) / halfLifeMs;
+
+ const decayedVisitCounts = new Map();
+ for (const [domain, count] of this.domainVisitCounts.entries()) {
+ const recentVisits = this.visitHistory.filter(r => r.domain === domain);
+ if (recentVisits.length === 0) continue;
+
+ let decayedCount = 0;
+ for (const visit of recentVisits) {
+ const ageMs = now - visit.timestamp;
+ decayedCount += Math.exp(-decayFactor * ageMs);
+ }
+
+ if (decayedCount > 0.01) {
+ decayedVisitCounts.set(domain, decayedCount);
+ }
+ }
+
+ this.domainVisitCounts = decayedVisitCounts;
+ console.log('⏰ Time decay applied to visit counts');
+ }
+
+ // ========== 训练数据生成 ==========
+
+ generateTrainingData() {
+ const trainingData = [];
+
+ for (const record of this.groupingHistory) {
+ if (!record.isManual) continue;
+
+ for (const tabInfo of record.tabs) {
+ const tab = {
+ id: tabInfo.id,
+ url: tabInfo.url,
+ title: tabInfo.title
+ };
+
+ trainingData.push({
+ tab,
+ label: record.groupName,
+ timestamp: record.timestamp
+ });
+ }
+ }
+
+ console.log('📚 Generated', trainingData.length, 'training samples from grouping history');
+ return trainingData;
+ }
+
+ extractDomain(url) {
+ if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
+ return null;
+ }
+
+ try {
+ const urlObj = new URL(url);
+ return urlObj.hostname;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ getStats() {
+ return {
+ groupingHistoryCount: this.groupingHistory.length,
+ visitHistoryCount: this.visitHistory.length,
+ domainCount: this.domainVisitCounts.size,
+ manualPreferencesCount: this.manualGroupPreferences.size,
+ contentTypePreferencesCount: this.contentTypePreferences.size,
+ sessionStartTime: this.sessionStartTime,
+ initialized: this.initialized
+ };
+ }
+
+ async clearHistory(daysToKeep = 30) {
+ const cutoffTime = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000);
+
+ this.groupingHistory = this.groupingHistory.filter(r => r.timestamp > cutoffTime);
+ this.visitHistory = this.visitHistory.filter(r => r.timestamp > cutoffTime);
+
+ const activeDomains = new Set(this.visitHistory.map(r => r.domain));
+
+ for (const domain of this.domainVisitCounts.keys()) {
+ if (!activeDomains.has(domain)) {
+ this.domainVisitCounts.delete(domain);
+ this.domainTotalTime.delete(domain);
+ }
+ }
+
+ await this.saveToStorage();
+ console.log('🧹 History cleared, keeping last', daysToKeep, 'days');
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = UserBehaviorTracker;
+}
+
+if (typeof window !== 'undefined') {
+ window.UserBehaviorTracker = UserBehaviorTracker;
+}