├── manifest.json ├── background.js ├── storage-config.js ├── moemail-service.js ├── notice-manager.js ├── README.md ├── popup.html ├── popup.css ├── popup-utils.js └── content-script.js /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "windsurf注册绑卡工具", 4 | "version": "1.0.0", 5 | "description": "Windsurf 自动注册工具 - 自动填写表单、接收验证码、管理账号", 6 | "permissions": [ 7 | "storage", 8 | "tabs", 9 | "activeTab", 10 | "downloads", 11 | "scripting" 12 | ], 13 | "host_permissions": [ 14 | "http://360stat.org/*", 15 | "https://windsurf.com/*", 16 | "https://codeium.com/*", 17 | "https://moemail.app/*" 18 | ], 19 | "action": { 20 | "default_popup": "popup.html", 21 | "default_title": "Windsurf 自动注册" 22 | }, 23 | "content_scripts": [ 24 | { 25 | "matches": [ 26 | "https://windsurf.com/*", 27 | "https://codeium.com/*" 28 | ], 29 | "js": ["storage-config.js", "content-script.js"], 30 | "run_at": "document_idle" 31 | } 32 | ], 33 | "background": { 34 | "service_worker": "background.js" 35 | }, 36 | "web_accessible_resources": [ 37 | { 38 | "resources": ["*.js"], 39 | "matches": [ 40 | "https://windsurf.com/*", 41 | "https://codeium.com/*" 42 | ] 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 后台服务脚本 3 | * 处理扩展的后台任务和消息传递 4 | */ 5 | 6 | importScripts('moemail-service.js'); 7 | 8 | // 监听扩展安装 9 | chrome.runtime.onInstalled.addListener((details) => { 10 | if (details.reason === 'install') { 11 | console.log('[扩展] 首次安装'); 12 | } else if (details.reason === 'update') { 13 | console.log('[扩展] 更新到新版本:', details.previousVersion); 14 | } 15 | }); 16 | 17 | // 监听扩展图标点击事件,打开独立窗口(这样点击页面时不会关闭) 18 | chrome.action.onClicked.addListener(async (tab) => { 19 | try { 20 | // 检查是否已经有打开的窗口 21 | const windows = await chrome.windows.getAll(); 22 | const existingWindow = windows.find(win => 23 | win.type === 'popup' && 24 | win.url && 25 | win.url.includes('popup.html') 26 | ); 27 | 28 | if (existingWindow) { 29 | // 如果窗口已存在,聚焦到该窗口 30 | await chrome.windows.update(existingWindow.id, { focused: true }); 31 | console.log('[扩展] 聚焦到已存在的窗口'); 32 | } else { 33 | // 创建新窗口 34 | const newWindow = await chrome.windows.create({ 35 | url: chrome.runtime.getURL('popup.html'), 36 | type: 'popup', 37 | width: 450, 38 | height: 700, 39 | focused: true 40 | }); 41 | console.log('[扩展] 创建新窗口:', newWindow.id); 42 | } 43 | } catch (error) { 44 | console.error('[扩展] 打开窗口失败:', error); 45 | } 46 | }); 47 | 48 | function logMoemail(type, message) { 49 | const timestamp = new Date().toISOString(); 50 | console.log(`[Moemail][${timestamp}][${type}] ${message}`); 51 | } 52 | 53 | async function handleMoemailRequest(request) { 54 | const payload = request.payload || {}; 55 | 56 | if (!MoemailService) { 57 | throw new Error('Moemail 服务未初始化'); 58 | } 59 | 60 | if (request.type === 'moemailCreate') { 61 | const name = payload.name || MoemailService.generateRandomAlphaNumeric(10); 62 | return MoemailService.createEmail({ name, log: logMoemail }); 63 | } 64 | 65 | if (request.type === 'moemailFetchMessages') { 66 | const { emailId, cursor } = payload; 67 | return MoemailService.fetchMessages(emailId, cursor || '', { log: logMoemail }); 68 | } 69 | 70 | if (request.type === 'moemailDelete') { 71 | const { emailId } = payload; 72 | await MoemailService.deleteEmail(emailId, { log: logMoemail }); 73 | return { deleted: true }; 74 | } 75 | 76 | return null; 77 | } 78 | 79 | // 监听来自 content script 或 popup 的消息 80 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 81 | const { type } = request || {}; 82 | 83 | if (!type) { 84 | sendResponse({ success: false, error: '缺少消息类型' }); 85 | return false; 86 | } 87 | 88 | if (type === 'ping') { 89 | sendResponse({ success: true, message: 'pong' }); 90 | return true; 91 | } 92 | 93 | if (type.startsWith('moemail')) { 94 | (async () => { 95 | try { 96 | const result = await handleMoemailRequest(request); 97 | sendResponse({ success: true, data: result }); 98 | } catch (error) { 99 | console.error('[后台] 处理 Moemail 请求失败:', error); 100 | sendResponse({ success: false, error: error.message }); 101 | } 102 | })(); 103 | return true; 104 | } 105 | 106 | // 其他消息 107 | try { 108 | console.log('[后台] 收到消息:', request); 109 | sendResponse({ success: true }); 110 | } catch (error) { 111 | console.error('[后台] 处理消息失败:', error); 112 | sendResponse({ success: false, error: error.message }); 113 | } 114 | 115 | return true; // 保持消息通道开放以支持异步响应 116 | }); 117 | 118 | // 错误处理 119 | self.addEventListener('error', (event) => { 120 | console.error('[后台] 发生错误:', event.error); 121 | }); 122 | 123 | self.addEventListener('unhandledrejection', (event) => { 124 | console.error('[后台] 未处理的 Promise 拒绝:', event.reason); 125 | }); 126 | 127 | -------------------------------------------------------------------------------- /storage-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储键名常量 3 | */ 4 | const STORAGE_KEYS = { 5 | DEVICE_CODE: 'windsurf_device_code', 6 | CARD_KEY: 'windsurf_card_key', 7 | EXPIRE_TIME: 'windsurf_expire_time', 8 | VERIFIED: 'windsurf_verified', 9 | VERIFIED_AT: 'windsurf_verified_at', 10 | NOTICE_CACHE: 'windsurf_notice_cache', 11 | NOTICE_TIMESTAMP: 'windsurf_notice_timestamp', 12 | NOTICE_LAST_FETCH: 'windsurf_notice_last_fetch', 13 | APP_CONFIG: 'windsurf_app_config', 14 | USER_SETTINGS: 'windsurf_user_settings' 15 | }; 16 | 17 | /** 18 | * 存储管理器 19 | * 封装 Chrome Storage API,提供错误处理 20 | */ 21 | class StorageManager { 22 | /** 23 | * 设置单个存储项 24 | * @param {string} key - 存储键 25 | * @param {*} value - 存储值 26 | * @returns {Promise} 27 | * @throws {Error} 存储失败时抛出错误 28 | */ 29 | static async set(key, value) { 30 | if (!key || typeof key !== 'string') { 31 | throw new Error('存储键必须是有效的字符串'); 32 | } 33 | 34 | try { 35 | await chrome.storage.local.set({ 36 | [key]: value 37 | }); 38 | } catch (error) { 39 | console.error('[存储] 设置失败:', error); 40 | throw new Error(`保存数据失败: ${error.message || '未知错误'}`); 41 | } 42 | } 43 | 44 | /** 45 | * 获取存储项 46 | * @param {string|string[]} keys - 存储键或键数组 47 | * @returns {Promise<*>} 存储值或对象 48 | * @throws {Error} 获取失败时抛出错误 49 | */ 50 | static async get(keys) { 51 | if (!keys) { 52 | throw new Error('存储键不能为空'); 53 | } 54 | 55 | try { 56 | const result = await chrome.storage.local.get(keys); 57 | 58 | // 如果传入单个字符串,返回对应的值 59 | if (typeof keys === 'string') { 60 | return result[keys]; 61 | } 62 | 63 | // 如果传入数组,返回对象 64 | return result; 65 | } catch (error) { 66 | console.error('[存储] 获取失败:', error); 67 | throw new Error(`读取数据失败: ${error.message || '未知错误'}`); 68 | } 69 | } 70 | 71 | /** 72 | * 删除存储项 73 | * @param {string|string[]} keys - 存储键或键数组 74 | * @returns {Promise} 75 | * @throws {Error} 删除失败时抛出错误 76 | */ 77 | static async remove(keys) { 78 | if (!keys) { 79 | throw new Error('存储键不能为空'); 80 | } 81 | 82 | try { 83 | await chrome.storage.local.remove(keys); 84 | } catch (error) { 85 | console.error('[存储] 删除失败:', error); 86 | throw new Error(`删除数据失败: ${error.message || '未知错误'}`); 87 | } 88 | } 89 | 90 | /** 91 | * 清空所有存储 92 | * @returns {Promise} 93 | * @throws {Error} 清空失败时抛出错误 94 | */ 95 | static async clear() { 96 | try { 97 | await chrome.storage.local.clear(); 98 | } catch (error) { 99 | console.error('[存储] 清空失败:', error); 100 | throw new Error(`清空存储失败: ${error.message || '未知错误'}`); 101 | } 102 | } 103 | 104 | /** 105 | * 批量设置存储项 106 | * @param {Object} items - 键值对对象 107 | * @returns {Promise} 108 | * @throws {Error} 设置失败时抛出错误 109 | */ 110 | static async setBatch(items) { 111 | if (!items || typeof items !== 'object') { 112 | throw new Error('批量设置的数据必须是对象'); 113 | } 114 | 115 | try { 116 | await chrome.storage.local.set(items); 117 | } catch (error) { 118 | console.error('[存储] 批量设置失败:', error); 119 | throw new Error(`批量保存数据失败: ${error.message || '未知错误'}`); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * API Key 管理器 126 | * 提供 Moemail API Key 的存储和获取功能 127 | */ 128 | class ApiKeyManager { 129 | /** 130 | * 保存 Moemail API Key 131 | * @param {string} apiKey - API Key 132 | * @returns {Promise} 133 | */ 134 | static async saveMoemailApiKey(apiKey) { 135 | if (typeof chrome !== 'undefined' && chrome.storage) { 136 | return StorageManager.set('moemail_api_key', apiKey); 137 | } else { 138 | // 兼容其他环境 139 | localStorage.setItem('moemail_api_key', apiKey); 140 | return Promise.resolve(); 141 | } 142 | } 143 | 144 | /** 145 | * 获取 Moemail API Key 146 | * @returns {Promise} API Key 147 | */ 148 | static async getMoemailApiKey() { 149 | if (typeof chrome !== 'undefined' && chrome.storage) { 150 | const value = await StorageManager.get('moemail_api_key'); 151 | return value || ''; 152 | } else { 153 | // 兼容其他环境 154 | return localStorage.getItem('moemail_api_key') || ''; 155 | } 156 | } 157 | 158 | /** 159 | * 删除 Moemail API Key 160 | * @returns {Promise} 161 | */ 162 | static async removeMoemailApiKey() { 163 | if (typeof chrome !== 'undefined' && chrome.storage) { 164 | return StorageManager.remove('moemail_api_key'); 165 | } else { 166 | // 兼容其他环境 167 | localStorage.removeItem('moemail_api_key'); 168 | return Promise.resolve(); 169 | } 170 | } 171 | } 172 | 173 | // 在页面加载完成后,从缓存中读取API Key并填充到输入框 174 | if (typeof document !== 'undefined') { 175 | document.addEventListener('DOMContentLoaded', async function() { 176 | try { 177 | const savedApiKey = await ApiKeyManager.getMoemailApiKey(); 178 | const apiKeyInput = document.getElementById('moemailApiKey'); 179 | if (apiKeyInput) { 180 | apiKeyInput.value = savedApiKey; 181 | } 182 | } catch (error) { 183 | console.error('获取保存的API Key失败:', error); 184 | } 185 | }); 186 | } -------------------------------------------------------------------------------- /moemail-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moemail API Service 3 | * 提供创建、查询和删除 moemail.app 临时邮箱的统一接口 4 | */ 5 | (function initMoemailService(globalScope) { 6 | const CONFIG = { 7 | BASE_URL: 'https://moemail.app/api/emails', 8 | API_KEY: '', 9 | DEFAULT_DOMAIN: 'moemail.app', 10 | DEFAULT_EXPIRY: 3600000 // 1 小时 11 | }; 12 | 13 | function logMessage(logFn, type, message) { 14 | if (typeof logFn === 'function') { 15 | try { 16 | logFn(type, message); 17 | } catch (error) { 18 | console.warn('[Moemail] 日志回调失败:', error); 19 | } 20 | } else { 21 | console.log(`[Moemail][${type}] ${message}`); 22 | } 23 | } 24 | 25 | async function getApiKey() { 26 | return new Promise((resolve) => { 27 | if (typeof chrome !== 'undefined' && chrome.storage) { 28 | chrome.storage.local.get(['moemail_api_key'], (result) => { 29 | resolve(result.moemail_api_key || CONFIG.API_KEY); 30 | }); 31 | } else { 32 | // 兼容其他环境 33 | const apiKey = localStorage.getItem('moemail_api_key') || CONFIG.API_KEY; 34 | resolve(apiKey); 35 | } 36 | }); 37 | } 38 | 39 | function buildHeaders(includeJson = false) { 40 | return getApiKey().then(apiKey => { 41 | const headers = { 42 | 'X-API-Key': apiKey 43 | }; 44 | 45 | if (includeJson) { 46 | headers['Content-Type'] = 'application/json; charset=utf-8'; 47 | } 48 | 49 | return headers; 50 | }); 51 | } 52 | 53 | function generateRandomAlphaNumeric(length = 8) { 54 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 55 | let result = ''; 56 | 57 | for (let i = 0; i < length; i++) { 58 | result += chars[Math.floor(Math.random() * chars.length)]; 59 | } 60 | 61 | return result; 62 | } 63 | 64 | function extractDigits(text = '') { 65 | const matches = text.match(/\d+/g); 66 | return matches ? matches.join('') : ''; 67 | } 68 | 69 | async function createEmail(options = {}) { 70 | const { name, expiryTime = CONFIG.DEFAULT_EXPIRY, domain = CONFIG.DEFAULT_DOMAIN, log } = options; 71 | 72 | const maxAttempts = 5; 73 | let attempt = 0; 74 | let lastError = null; 75 | 76 | while (attempt < maxAttempts) { 77 | const currentName = attempt === 0 && name ? name : generateRandomAlphaNumeric(12); 78 | const payload = { 79 | name: currentName, 80 | expiryTime, 81 | domain 82 | }; 83 | 84 | const headers = await buildHeaders(true); 85 | const response = await fetch(`${CONFIG.BASE_URL}/generate`, { 86 | method: 'POST', 87 | headers: headers, 88 | body: JSON.stringify(payload) 89 | }); 90 | 91 | const responseText = await response.text(); 92 | 93 | if (response.ok) { 94 | let data = null; 95 | try { 96 | data = JSON.parse(responseText); 97 | } catch (error) { 98 | throw new Error('Moemail 返回数据解析失败: ' + error.message); 99 | } 100 | 101 | if (!data?.email || !data?.id) { 102 | throw new Error('Moemail 返回数据缺少 email 或 id 字段'); 103 | } 104 | 105 | logMessage(log, 'success', `邮箱创建成功: ${data.email}`); 106 | 107 | return { 108 | email: data.email.replace(/"/g, ''), 109 | id: data.id.replace(/"/g, ''), 110 | raw: data 111 | }; 112 | } 113 | 114 | lastError = new Error(`Moemail 创建失败: ${response.status} ${response.statusText} - ${responseText}`); 115 | 116 | if (response.status === 409) { 117 | logMessage(log, 'warn', `邮箱名称已被占用,正在重试...(第 ${attempt + 1} 次)`); 118 | attempt += 1; 119 | continue; 120 | } 121 | 122 | throw lastError; 123 | } 124 | 125 | throw lastError || new Error('Moemail 创建失败:多次尝试仍未成功,请稍后重试'); 126 | } 127 | 128 | async function fetchMessages(emailId, cursor = '', options = {}) { 129 | const { log } = options; 130 | 131 | if (!emailId) { 132 | throw new Error('Moemail 邮箱 ID 不能为空'); 133 | } 134 | 135 | const headers = await buildHeaders(false); 136 | const url = `${CONFIG.BASE_URL}/${encodeURIComponent(emailId)}?cursor=${encodeURIComponent(cursor || '')}`; 137 | const response = await fetch(url, { 138 | method: 'GET', 139 | headers: headers 140 | }); 141 | 142 | const responseBuffer = await response.arrayBuffer(); 143 | const decoder = new TextDecoder('utf-8'); 144 | const responseText = decoder.decode(responseBuffer); 145 | 146 | if (!response.ok) { 147 | throw new Error(`Moemail 获取邮件失败: ${response.status} ${response.statusText} - ${responseText}`); 148 | } 149 | 150 | let data = null; 151 | try { 152 | data = JSON.parse(responseText); 153 | } catch (error) { 154 | throw new Error('Moemail 邮件数据解析失败: ' + error.message); 155 | } 156 | 157 | const messages = Array.isArray(data?.messages) ? data.messages : []; 158 | const latestMessage = messages[0] || null; 159 | const latestCode = latestMessage ? extractDigits(latestMessage.subject || latestMessage.text || '') : ''; 160 | 161 | if (latestMessage) { 162 | logMessage(log, 'success', '获取到最新邮件'); 163 | } else { 164 | logMessage(log, 'info', '暂无新邮件'); 165 | } 166 | 167 | return { 168 | messages, 169 | cursor: data?.cursor || data?.nextCursor || '', 170 | latestCode, 171 | latestMessage, 172 | raw: data 173 | }; 174 | } 175 | 176 | async function deleteEmail(emailId, options = {}) { 177 | const { log } = options; 178 | 179 | if (!emailId) { 180 | throw new Error('Moemail 邮箱 ID 不能为空'); 181 | } 182 | 183 | const headers = await buildHeaders(false); 184 | const url = `${CONFIG.BASE_URL}/${encodeURIComponent(emailId)}`; 185 | const response = await fetch(url, { 186 | method: 'DELETE', 187 | headers: headers 188 | }); 189 | 190 | if (!response.ok) { 191 | const text = await response.text(); 192 | throw new Error(`Moemail 删除失败: ${response.status} ${response.statusText} - ${text}`); 193 | } 194 | 195 | logMessage(log, 'success', `邮箱已删除: ${emailId}`); 196 | } 197 | 198 | const MoemailService = { 199 | CONFIG, 200 | generateRandomAlphaNumeric, 201 | extractDigits, 202 | buildHeaders, 203 | createEmail, 204 | fetchMessages, 205 | deleteEmail 206 | }; 207 | 208 | globalScope.MoemailService = MoemailService; 209 | })(typeof self !== 'undefined' ? self : this); 210 | 211 | -------------------------------------------------------------------------------- /notice-manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 公告管理器 3 | * 负责获取、缓存和管理公告信息 4 | */ 5 | class NoticeManager { 6 | /** 7 | * 获取公告(优先返回缓存,后台更新) 8 | * @returns {Promise} 公告数据或null 9 | */ 10 | async getNotice() { 11 | try { 12 | // 先获取缓存 13 | const cachedNotice = await this.getCachedNotice(); 14 | 15 | // 后台异步更新(不阻塞) 16 | this.fetchAndUpdateNotice().catch(error => { 17 | console.warn('[公告] 后台更新失败:', error); 18 | // 后台更新失败不影响使用,静默处理 19 | }); 20 | 21 | // 如果有缓存,立即返回 22 | if (cachedNotice) { 23 | return cachedNotice; 24 | } 25 | 26 | // 没有缓存,等待获取 27 | return await this.fetchAndUpdateNotice(); 28 | } catch (error) { 29 | console.error('[公告] 获取公告失败:', error); 30 | // 尝试返回缓存 31 | try { 32 | return await this.getCachedNotice(); 33 | } catch (cacheError) { 34 | console.error('[公告] 获取缓存也失败:', cacheError); 35 | return null; 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * 获取缓存的公告 42 | * @returns {Promise} 缓存的公告数据或null 43 | */ 44 | async getCachedNotice() { 45 | try { 46 | const cached = await StorageManager.get(STORAGE_KEYS.NOTICE_CACHE); 47 | 48 | // 验证缓存数据格式 49 | if (cached && typeof cached === 'object') { 50 | return cached; 51 | } 52 | 53 | return null; 54 | } catch (error) { 55 | console.error('[公告] 读取缓存失败:', error); 56 | return null; 57 | } 58 | } 59 | 60 | /** 61 | * 获取并更新公告 62 | * @returns {Promise} 公告数据或null 63 | */ 64 | async fetchAndUpdateNotice() { 65 | try { 66 | const response = await cardKeyAPI.getConfig(); 67 | 68 | // 验证响应格式 69 | if (!response || typeof response !== 'object') { 70 | throw new Error('服务器返回数据格式错误'); 71 | } 72 | 73 | if (response.status !== 1 || !response.result) { 74 | throw new Error('服务器返回数据异常: status=' + response.status); 75 | } 76 | 77 | const noticeData = response.result; 78 | const timestamp = response.ts; 79 | 80 | // 验证时间戳 81 | if (!timestamp) { 82 | console.warn('[公告] 服务器未返回时间戳'); 83 | } 84 | 85 | // 检查是否需要更新 86 | const needUpdate = await this.compareAndCheckUpdate(noticeData, timestamp); 87 | 88 | if (needUpdate) { 89 | try { 90 | await this.saveNoticeCache(noticeData, timestamp); 91 | this.notifyUIUpdate(noticeData); 92 | } catch (saveError) { 93 | console.error('[公告] 保存缓存失败:', saveError); 94 | // 保存失败不影响返回数据 95 | } 96 | } 97 | 98 | return noticeData; 99 | } catch (error) { 100 | console.error('[公告] 获取失败:', error); 101 | 102 | // 尝试返回缓存作为降级方案 103 | const cached = await this.getCachedNotice(); 104 | if (cached) { 105 | console.info('[公告] 使用缓存数据'); 106 | return cached; 107 | } 108 | 109 | // 没有缓存,返回null 110 | return null; 111 | } 112 | } 113 | 114 | /** 115 | * 比较并检查是否需要更新 116 | * @param {Object} newData - 新的公告数据 117 | * @param {string|number} newTimestamp - 新的时间戳 118 | * @returns {Promise} 是否需要更新 119 | */ 120 | async compareAndCheckUpdate(newData, newTimestamp) { 121 | try { 122 | const storageData = await StorageManager.get([ 123 | STORAGE_KEYS.NOTICE_CACHE, 124 | STORAGE_KEYS.NOTICE_TIMESTAMP 125 | ]); 126 | 127 | const cachedData = storageData[STORAGE_KEYS.NOTICE_CACHE]; 128 | const cachedTimestamp = storageData[STORAGE_KEYS.NOTICE_TIMESTAMP]; 129 | 130 | // 如果没有缓存,需要更新 131 | if (!cachedData) { 132 | return true; 133 | } 134 | 135 | // 如果时间戳不同,需要更新 136 | if (cachedTimestamp !== newTimestamp) { 137 | return true; 138 | } 139 | 140 | // 比较公告内容 141 | const newNotice = newData?.notice; 142 | const cachedNotice = cachedData?.notice; 143 | 144 | if (!cachedNotice) { 145 | return true; 146 | } 147 | 148 | // 比较公告字段 149 | const noticeFields = [ 150 | 'title', 'message', 'positiveText', 'cancelText', 151 | 'neutralText', 'ext', 'cancelExt', 'neutralExt' 152 | ]; 153 | 154 | for (const field of noticeFields) { 155 | if (newNotice?.[field] !== cachedNotice[field]) { 156 | return true; 157 | } 158 | } 159 | 160 | // 比较注册信息 161 | const newRegister = newData?.register; 162 | const cachedRegister = cachedData?.register; 163 | 164 | if (newRegister?.message !== cachedRegister?.message) { 165 | return true; 166 | } 167 | 168 | // 内容相同,不需要更新 169 | return false; 170 | } catch (error) { 171 | console.error('[公告] 对比失败:', error); 172 | // 出错时默认需要更新,确保数据最新 173 | return true; 174 | } 175 | } 176 | 177 | /** 178 | * 保存公告缓存 179 | * @param {Object} noticeData - 公告数据 180 | * @param {string|number} timestamp - 时间戳 181 | * @returns {Promise} 182 | * @throws {Error} 保存失败时抛出错误 183 | */ 184 | async saveNoticeCache(noticeData, timestamp) { 185 | if (!noticeData || typeof noticeData !== 'object') { 186 | throw new Error('公告数据格式错误'); 187 | } 188 | 189 | try { 190 | await StorageManager.setBatch({ 191 | [STORAGE_KEYS.NOTICE_CACHE]: noticeData, 192 | [STORAGE_KEYS.NOTICE_TIMESTAMP]: timestamp, 193 | [STORAGE_KEYS.NOTICE_LAST_FETCH]: new Date().toISOString() 194 | }); 195 | } catch (error) { 196 | console.error('[公告] 保存缓存失败:', error); 197 | throw new Error('保存公告缓存失败: ' + error.message); 198 | } 199 | } 200 | 201 | /** 202 | * 通知UI更新 203 | * @param {Object} noticeData - 公告数据 204 | */ 205 | notifyUIUpdate(noticeData) { 206 | try { 207 | if (typeof document !== 'undefined' && document.dispatchEvent) { 208 | const event = new CustomEvent('noticeUpdated', { 209 | detail: { 210 | notice: noticeData 211 | } 212 | }); 213 | document.dispatchEvent(event); 214 | } 215 | } catch (error) { 216 | console.error('[公告] 通知UI更新失败:', error); 217 | // 通知失败不影响功能,静默处理 218 | } 219 | } 220 | 221 | /** 222 | * 清除缓存 223 | * @returns {Promise} 224 | */ 225 | async clearCache() { 226 | try { 227 | await StorageManager.remove([ 228 | STORAGE_KEYS.NOTICE_CACHE, 229 | STORAGE_KEYS.NOTICE_TIMESTAMP, 230 | STORAGE_KEYS.NOTICE_LAST_FETCH 231 | ]); 232 | } catch (error) { 233 | console.error('[公告] 清除缓存失败:', error); 234 | throw new Error('清除公告缓存失败: ' + error.message); 235 | } 236 | } 237 | 238 | /** 239 | * 强制刷新公告 240 | * @returns {Promise} 公告数据或null 241 | */ 242 | async forceRefresh() { 243 | try { 244 | await this.clearCache(); 245 | return await this.fetchAndUpdateNotice(); 246 | } catch (error) { 247 | console.error('[公告] 强制刷新失败:', error); 248 | throw error; 249 | } 250 | } 251 | } 252 | 253 | const noticeManager = new NoticeManager(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windsurf 注册绑卡工具 2 | 3 | 一个功能强大的 Chrome 浏览器扩展,用于自动注册和管理 Windsurf (Codeium) 账号。 4 | 5 | ## 📋 项目简介 6 | 7 | 本扩展提供了自动化的 Windsurf 账号注册功能,包括: 8 | - 自动填写注册表单 9 | - 自动生成和管理邮箱(基于 Moemail API) 10 | - 自动接收邮件验证码 11 | - 账号信息导出 12 | - 公告通知系统 13 | - 美观的现代化 UI 界面 14 | 15 | **版本**: V1.0.0 16 | 17 | ## 🚀 功能特性 18 | 19 | ### 核心功能 20 | 21 | 1. **自动注册** 22 | - 自动打开并检测 Windsurf/Codeium 注册页面 23 | - 自动生成随机邮箱前缀并在 Moemail 平台创建临时邮箱 24 | - 无需手动维护域名/序号,所有邮箱由插件实时创建 25 | - 表单信息填充、提交和状态回传全自动 26 | - 支持人机验证后的自动继续 27 | 28 | 2. **邮箱管理** 29 | - 自动生成随机邮箱前缀(15位大小写字母+数字) 30 | - 一键复制邮箱地址 31 | - 实时预览邮箱地址 32 | - 支持设置邮箱密码(可选) 33 | - 自动创建 Moemail 临时邮箱 34 | 35 | 3. **密码管理** 36 | - 自动生成安全密码(12-16位,包含大小写、数字、符号) 37 | - 密码显示/隐藏切换 38 | - 登录密码和邮箱密码独立管理 39 | - 一键生成随机密码 40 | 41 | 4. **邮件验证** 42 | - 深度集成 Moemail API:创建、轮询、删除全自动 43 | - 验证码到达后立即填入页面并尝试提交 44 | - 根据页面结构自动适配单输入框/多输入框验证码组件 45 | - 自动清理临时邮箱 46 | 47 | 5. **API Key 管理** 48 | - 安全存储 Moemail API Key 49 | - API Key 显示/隐藏切换 50 | - 一键测试 API Key 有效性 51 | - 自动保存到浏览器缓存 52 | 53 | 6. **账号管理** 54 | - 导出账号信息到桌面(JSON 格式) 55 | - 自动保存配置 56 | - 账号列表管理 57 | 58 | 7. **公告系统** 59 | - 自动获取服务器公告 60 | - 本地缓存机制 61 | - 智能更新检测 62 | 63 | 8. **UI 界面** 64 | - 现代化设计风格 65 | - 响应式布局 66 | - SVG 图标系统 67 | - 流畅的动画效果 68 | - 紧凑的布局设计 69 | 70 | ## 📦 安装说明 71 | 72 | ### 方法一:开发者模式安装(推荐) 73 | 74 | 1. **下载项目文件** 75 | - 将所有项目文件下载到本地目录 76 | 77 | 2. **打开 Chrome 扩展管理页面** 78 | - 在 Chrome 浏览器地址栏输入:`chrome://extensions/` 79 | - 或通过菜单:`更多工具` → `扩展程序` 80 | 81 | 3. **启用开发者模式** 82 | - 在扩展管理页面右上角,打开"开发者模式"开关 83 | 84 | 4. **加载扩展** 85 | - 点击"加载已解压的扩展程序"按钮 86 | - 选择项目文件所在的目录 87 | - 点击"选择文件夹" 88 | 89 | 5. **验证安装** 90 | - 扩展应该出现在扩展列表中 91 | - 浏览器工具栏会显示扩展图标 92 | 93 | ### 方法二:打包安装 94 | 95 | 1. 在扩展管理页面点击"打包扩展程序" 96 | 2. 选择项目根目录 97 | 3. 生成 `.crx` 文件后,可以分发给其他用户安装 98 | 99 | ## 📦 打包插件 100 | 101 | ### 方法一:使用 Chrome 扩展管理页面打包(推荐) 102 | 103 | 1. **打开扩展管理页面** 104 | - 在 Chrome 浏览器地址栏输入:`chrome://extensions/` 105 | - 或通过菜单:`更多工具` → `扩展程序` 106 | 107 | 2. **启用开发者模式** 108 | - 在扩展管理页面右上角,确保"开发者模式"开关已打开 109 | 110 | 3. **打包扩展** 111 | - 点击页面上的"打包扩展程序"按钮 112 | - 在"扩展程序根目录"中选择项目文件夹(包含 manifest.json 的目录) 113 | - "私有密钥文件"留空(首次打包会自动生成) 114 | - 点击"打包扩展程序"按钮 115 | 116 | 4. **获取打包文件** 117 | - 打包成功后,会在项目目录生成两个文件: 118 | - `.crx` 文件:扩展程序安装包(可以分发给用户) 119 | - `.pem` 文件:私有密钥文件(请妥善保管,用于后续更新) 120 | 121 | 5. **分发安装包** 122 | - 将 `.crx` 文件分发给用户 123 | - 用户可以通过拖拽 `.crx` 文件到 `chrome://extensions/` 页面来安装 124 | 125 | ### 方法二:使用命令行打包 126 | 127 | 1. **使用 Chrome 的打包工具** 128 | ```bash 129 | # Windows 130 | "C:\Program Files\Google\Chrome\Application\chrome.exe" --pack-extension="C:\Users\Administrator\Desktop\浏览器插件" 131 | 132 | # macOS 133 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --pack-extension="/path/to/浏览器插件" 134 | 135 | # Linux 136 | google-chrome --pack-extension="/path/to/浏览器插件" 137 | ``` 138 | 139 | 2. **打包结果** 140 | - 会在项目目录生成 `.crx` 和 `.pem` 文件 141 | 142 | ### 打包注意事项 143 | 144 | 1. **文件完整性** 145 | - 确保所有必需文件都在项目目录中 146 | - 检查 `manifest.json` 中引用的所有文件是否存在 147 | 148 | 2. **私有密钥文件(.pem)** 149 | - 首次打包会自动生成 `.pem` 文件 150 | - 后续更新扩展时需要使用相同的 `.pem` 文件 151 | - 请妥善保管,丢失后无法更新已发布的扩展 152 | 153 | 3. **版本号管理** 154 | - 更新扩展时,记得在 `manifest.json` 中更新版本号 155 | - 使用相同的 `.pem` 文件打包 156 | 157 | 4. **测试打包结果** 158 | - 打包后建议先测试安装 159 | - 检查所有功能是否正常 160 | 161 | ## 🎯 使用指南 162 | 163 | ### 首次使用 164 | 165 | 1. **配置 Moemail API Key** 166 | - 在"密码与API配置"区域输入您的 Moemail API Key 167 | - 点击测试按钮验证 API Key 是否有效 168 | - API Key 会在保存配置时自动存储 169 | 170 | 2. **配置邮箱** 171 | - 在"邮箱配置"区域: 172 | - 点击生成图标自动生成随机邮箱前缀 173 | - 或手动输入邮箱用户名 174 | - 域名固定为 `moemail.app`,插件会自动创建临时邮箱 175 | - 可选:设置邮箱密码(点击生成图标可自动生成) 176 | 177 | 3. **设置登录密码** 178 | - 在"密码与API配置"区域: 179 | - 输入密码(至少 8 位) 180 | - 或点击生成图标自动生成安全密码 181 | - 可以点击眼睛图标显示/隐藏密码 182 | 183 | 4. **开始注册** 184 | - 点击"打开"按钮打开注册页面 185 | - 或手动访问 https://windsurf.com/account/register 186 | - 点击"开始自动注册"按钮 187 | - 剩余流程(表单填写、人机检测、验证码获取)将自动执行 188 | 189 | ### 界面功能说明 190 | 191 | #### 控制区域 192 | 193 | - **开始自动注册**(主按钮) 194 | - 启动完整的自动注册流程 195 | - 自动创建邮箱、填写表单、接收验证码 196 | 197 | - **填卡**(次要按钮) 198 | - 自动填写信用卡信息 199 | - 需要在账单页面使用 200 | 201 | - **打开**(次要按钮) 202 | - 快速打开 Windsurf 注册页面 203 | - 如果当前页面已是注册页面,则提示已就绪 204 | 205 | - **保存**(次要按钮) 206 | - 保存当前配置(邮箱前缀、密码、API Key) 207 | - 配置会自动保存到浏览器本地存储 208 | 209 | - **导出**(次要按钮) 210 | - 导出所有已保存的账号信息 211 | - 以 JSON 格式保存到桌面 212 | 213 | #### 邮箱配置区域 214 | 215 | - **邮箱用户名** 216 | - 输入框:手动输入邮箱前缀 217 | - 生成按钮:自动生成 15 位随机前缀(大小写字母+数字) 218 | 219 | - **邮箱域名** 220 | - 显示为 `moemail.app · 自动创建` 221 | - 系统会自动在 Moemail 创建临时邮箱 222 | 223 | - **预览邮箱** 224 | - 实时显示完整邮箱地址 225 | - 复制按钮:一键复制邮箱到剪贴板 226 | 227 | - **邮箱密码**(可选) 228 | - 输入框:设置邮箱密码 229 | - 眼睛图标:显示/隐藏密码 230 | - 生成图标:自动生成随机密码 231 | 232 | #### 密码与API配置区域 233 | 234 | - **登录密码** 235 | - 输入框:设置 Windsurf 账号登录密码 236 | - 眼睛图标:显示/隐藏密码 237 | - 生成图标:自动生成安全密码(12-16位) 238 | 239 | - **Moemail API Key** 240 | - 输入框:输入您的 Moemail API Key 241 | - 眼睛图标:显示/隐藏 API Key 242 | - 测试图标:验证 API Key 是否有效 243 | - 提示:API Key 将在保存配置时自动存储 244 | 245 | #### 运行状态区域 246 | 247 | - **状态显示** 248 | - 实时显示当前运行状态 249 | - 支持成功、错误、运行中等状态样式 250 | 251 | - **实时日志** 252 | - 显示详细的操作日志 253 | - 支持信息、成功、错误等日志类型 254 | - 清空按钮:一键清空所有日志 255 | 256 | #### 使用说明区域 257 | 258 | - **可折叠说明** 259 | - 点击标题可展开/折叠使用说明 260 | - 包含详细的使用步骤和注意事项 261 | 262 | ### 功能说明 263 | 264 | #### 邮箱配置 265 | 266 | - **邮箱用户名**:可以手动输入或点击生成图标自动生成 15 位混合字符串 267 | - **邮箱域名**:固定为 `moemail.app`,无需手动管理,验证码也会同步回填 268 | - **复制邮箱**:点击复制按钮可快速复制完整邮箱地址到剪贴板 269 | 270 | #### 邮件验证 271 | 272 | - 插件通过 Moemail API 创建临时邮箱、轮询验证码并在页面中自动填写 273 | - 发生网络/人机失败时会在浮动面板提示,并自动清理临时邮箱 274 | - 支持单输入框和多输入框验证码格式 275 | 276 | #### API Key 管理 277 | 278 | - 首次使用需要配置 Moemail API Key 279 | - API Key 会加密存储到浏览器本地 280 | - 支持测试 API Key 有效性 281 | - 保存配置时会自动保存 API Key 282 | 283 | #### 账号导出 284 | 285 | - 点击"导出"按钮 286 | - 账号信息会以 JSON 格式保存到桌面 287 | - 包含:邮箱、密码、注册时间等信息 288 | 289 | ## 📁 项目结构 290 | 291 | ``` 292 | 浏览器插件/ 293 | ├── manifest.json # 扩展配置文件 294 | ├── popup.html # 弹窗界面 295 | ├── popup.css # 样式文件(现代化UI设计) 296 | ├── popup-utils.js # 弹窗核心逻辑 + 自动注册入口 297 | ├── background.js # Service Worker:Moemail API 调度 298 | ├── storage-config.js # 存储配置管理 + API Key 管理 299 | ├── notice-manager.js # 公告管理器 300 | ├── content-script.js # 注册页面自动化脚本 301 | ├── moemail-service.js # Moemail API 封装 302 | └── README.md # 说明文档 303 | ``` 304 | 305 | ## 🔧 技术架构 306 | 307 | ### 核心技术 308 | 309 | - **Manifest V3**: 使用最新的 Chrome 扩展 API 310 | - **Chrome Storage API**: 本地数据存储 311 | - **Fetch API**: HTTP 请求 312 | - **ES6+**: 现代 JavaScript 语法 313 | - **SVG Icons**: 矢量图标系统 314 | - **CSS3**: 现代化样式和动画 315 | 316 | ### 模块说明 317 | 318 | #### popup-utils.js 319 | - 主要业务逻辑 320 | - UI 事件处理 321 | - 自动注册流程控制 322 | - 邮箱和密码生成 323 | - 配置管理 324 | 325 | #### background.js 326 | - Service Worker 后台服务 327 | - Moemail API 请求处理 328 | - 消息路由和转发 329 | 330 | #### storage-config.js 331 | - 封装 Chrome Storage API 332 | - 提供统一的存储接口 333 | - 包含错误处理和数据验证 334 | - API Key 管理功能 335 | 336 | #### content-script.js 337 | - 注册页面自动化脚本 338 | - 表单填写和提交 339 | - 验证码自动填入 340 | - 浮动面板显示 341 | 342 | #### moemail-service.js 343 | - Moemail API 封装 344 | - 邮箱创建、查询、删除 345 | - 消息轮询和验证码提取 346 | 347 | #### notice-manager.js 348 | - 获取和管理服务器公告 349 | - 实现缓存机制和智能更新 350 | - 通过事件通知 UI 更新 351 | 352 | ## ⚙️ 配置说明 353 | 354 | ### 存储键名 355 | 356 | 所有存储键名定义在 `storage-config.js` 中: 357 | 358 | - `moemail_api_key`: Moemail API Key 359 | - `emailPrefix`: 邮箱前缀 360 | - `password`: 登录密码 361 | - `savedAccounts`: 已保存的账号列表 362 | - `NOTICE_CACHE`: 公告缓存 363 | - `NOTICE_TIMESTAMP`: 公告时间戳 364 | - `NOTICE_LAST_FETCH`: 最后获取时间 365 | - `APP_CONFIG`: 应用配置 366 | - `USER_SETTINGS`: 用户设置 367 | 368 | ### UI 配置 369 | 370 | - **主色调**: 橙色 (#F45805) 371 | - **强调色**: 蓝色 (#0460F7) 372 | - **圆角**: 14px (大) / 10px (中) / 8px (小) 373 | - **间距**: 紧凑布局,优化空间利用 374 | 375 | ## 🛠️ 开发说明 376 | 377 | ### 代码规范 378 | 379 | - 使用 ES6+ 语法 380 | - 添加 JSDoc 注释 381 | - 统一的错误处理 382 | - 清晰的变量命名 383 | - 模块化代码结构 384 | 385 | ### 错误处理 386 | 387 | 所有模块都实现了完善的错误处理: 388 | 389 | - **网络错误**: 自动重试和降级方案 390 | - **超时错误**: 10秒超时机制 391 | - **数据验证**: 输入验证和格式检查 392 | - **用户提示**: 友好的错误消息 393 | - **日志记录**: 详细的操作日志 394 | 395 | ### 调试 396 | 397 | 1. 打开 Chrome 开发者工具 398 | 2. 查看 Console 标签页 399 | 3. 所有日志都带有模块前缀,如:`[工具]`、`[Moemail]`、`[公告]` 等 400 | 4. 查看 Network 标签页检查 API 请求 401 | 5. 查看 Application 标签页检查存储数据 402 | 403 | ## ⚠️ 注意事项 404 | 405 | 1. **网络连接** 406 | - 需要稳定的网络连接 407 | - Moemail API 需要 HTTPS 连接 408 | 409 | 2. **权限要求** 410 | - 需要访问 Windsurf/Codeium 网站 411 | - 需要访问 Moemail API 服务器 412 | - 需要存储权限保存配置 413 | - 需要下载权限导出账号 414 | 415 | 3. **浏览器兼容性** 416 | - 仅支持 Chrome/Edge(Chromium 内核) 417 | - 需要 Manifest V3 支持 418 | - 建议使用最新版本浏览器 419 | 420 | 4. **API Key 安全** 421 | - API Key 存储在浏览器本地 422 | - 请勿分享您的 API Key 423 | - 定期检查 API Key 有效性 424 | 425 | 5. **使用限制** 426 | - 请遵守 Windsurf/Codeium 的使用条款 427 | - 请遵守 Moemail 的服务条款 428 | - 合理使用自动化功能 429 | 430 | ## 🐛 常见问题 431 | 432 | ### Q: 无法自动接收验证码? 433 | 434 | A: 435 | - 检查 Moemail API Key 是否正确配置 436 | - 点击测试按钮验证 API Key 是否有效 437 | - 检查网络连接是否正常 438 | - 查看控制台日志获取详细错误信息 439 | - 确认 Moemail 服务是否可用 440 | 441 | ### Q: 注册失败? 442 | 443 | A: 444 | - 检查 Windsurf 网站是否可访问 445 | - 确认邮箱格式是否正确 446 | - 检查密码是否符合要求(至少8位) 447 | - 查看控制台日志获取详细错误信息 448 | - 确认人机验证是否完成 449 | 450 | ### Q: 扩展无法加载? 451 | 452 | A: 453 | - 确认所有文件都在同一目录 454 | - 检查 manifest.json 格式是否正确 455 | - 查看扩展管理页面的错误信息 456 | - 确认浏览器版本是否支持 Manifest V3 457 | 458 | ### Q: 按钮点击无反应? 459 | 460 | A: 461 | - 刷新扩展页面 462 | - 检查控制台是否有错误信息 463 | - 确认相关脚本文件是否正常加载 464 | - 尝试重新加载扩展 465 | 466 | ### Q: API Key 测试失败? 467 | 468 | A: 469 | - 确认 API Key 格式是否正确 470 | - 检查 API Key 是否有效 471 | - 确认网络连接正常 472 | - 查看 Moemail 服务状态 473 | 474 | ## 📝 更新日志 475 | 476 | ### V1.0.0 (最新) 477 | 478 | #### UI 优化 479 | - ✅ 现代化 UI 设计,采用渐变和阴影效果 480 | - ✅ 统一使用 SVG 图标系统,替换 emoji 481 | - ✅ 优化布局,紧凑设计节省空间 482 | - ✅ 添加流畅的动画和过渡效果 483 | - ✅ 优化卡片和按钮的视觉层次 484 | 485 | #### 功能增强 486 | - ✅ 添加邮箱复制功能 487 | - ✅ 添加 API Key 测试功能 488 | - ✅ 添加邮箱密码生成功能 489 | - ✅ 添加日志清空功能 490 | - ✅ 优化配置保存逻辑(自动保存 API Key) 491 | - ✅ 优化按钮布局(4个次要按钮合并为一行) 492 | 493 | #### 代码优化 494 | - ✅ 修复所有按钮的事件绑定 495 | - ✅ 优化代码结构和可维护性 496 | - ✅ 完善错误处理机制 497 | - ✅ 添加用户友好的错误提示 498 | - ✅ 统一图标样式和大小 499 | 500 | #### 基础功能 501 | - ✅ 添加 manifest.json 配置文件 502 | - ✅ 代码去混淆和重构 503 | - ✅ 移除卡密验证功能 504 | - ✅ 更新名称为"windsurf注册绑卡工具" 505 | 506 | ## 📄 许可证 507 | 508 | 本项目仅供学习和研究使用。 509 | 510 | ## 🤝 贡献 511 | 512 | 欢迎提交 Issue 和 Pull Request。 513 | 514 | ## 📧 联系方式 515 | 516 | 如有问题或建议,请通过以下方式联系: 517 | - 查看项目 Issues 518 | - 提交 Pull Request 519 | 520 | --- 521 | 522 | **免责声明**: 本工具仅供学习研究使用,请遵守相关网站的使用条款。使用本工具产生的任何后果由使用者自行承担。 523 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | windsurf注册绑卡工具 7 | 8 | 9 | 10 |
11 |
12 |
V1.0.0
13 |

🌊 Windsurf 工具

14 |

智能注册助手,一键搞定邮箱与密码配置

15 |
16 | 17 | 18 |
19 | 24 |
25 | 29 | 33 | 37 | 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 |
50 |

邮箱配置

51 |
52 | 53 | 63 | 64 | 70 | 71 |
72 |
73 | test@moemail.app 74 | 77 |
78 |
79 | 80 | 94 |
95 | 96 | 97 |
98 |
99 |
100 | 101 |
102 |

密码与API配置

103 |
104 | 105 | 119 | 120 | 135 |
136 | 137 | 138 |
139 |
140 |
141 | 142 |
143 |

运行状态

144 |
145 |
等待开始...
146 |
147 |
148 | 实时日志 149 | 152 |
153 |
154 |
155 |
156 | 157 | 158 |
159 |
160 |
161 | 162 |
163 |

使用说明

164 |
165 | 181 |
182 | 183 | 184 | 197 |
198 | 199 | 200 | 201 | 202 | 203 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* 主色调 */ 3 | --primary-hue: 22; 4 | --primary-sat: 92%; 5 | --primary-lit: 50%; 6 | --primary-color: hsl(var(--primary-hue), var(--primary-sat), var(--primary-lit)); /* #F45805 */ 7 | --primary-color-light: hsl(var(--primary-hue), 100%, 60%); /* #FF8A34 */ 8 | --primary-color-faded: hsla(var(--primary-hue), var(--primary-sat), var(--primary-lit), 0.12); 9 | --primary-color-faded-more: hsla(var(--primary-hue), var(--primary-sat), var(--primary-lit), 0.08); 10 | 11 | /* 强调色 */ 12 | --accent-blue: #0460F7; 13 | --accent-blue-faded: rgba(4, 132, 247, 0.08); 14 | 15 | /* 文本颜色 */ 16 | --text-strong: #1f2430; 17 | --text-normal: #272b3a; 18 | --text-muted: #5f6377; 19 | --text-subtle: #8b91a5; 20 | --text-on-primary: #fff; 21 | 22 | /* 背景与表面颜色 */ 23 | --bg-page: #f3f5f9; 24 | --bg-container: #ffffff; 25 | --bg-card: #fdfdfd; 26 | --bg-input: #ffffff; 27 | --bg-tonal: #f3f4f8; 28 | --bg-tonal-hover: #e6e8f1; 29 | --bg-log: #0f172a; 30 | 31 | /* 边框颜色 */ 32 | --border-color: #e1e4ec; 33 | --border-color-light: #edf0f6; 34 | --border-color-focus: var(--primary-color); 35 | 36 | /* 状态色 */ 37 | --status-success: #04A777; 38 | --status-error: #D90429; 39 | --status-running: #FFB200; 40 | 41 | /* 字体 */ 42 | --font-sans: 'Inter', 'Segoe UI', Roboto, -apple-system, BlinkMacSystemFont, sans-serif; 43 | --font-mono: 'JetBrains Mono', Consolas, monospace; 44 | 45 | /* 尺寸与圆角 */ 46 | --radius-lg: 14px; 47 | --radius-md: 10px; 48 | --radius-sm: 8px; 49 | 50 | /* 阴影 */ 51 | --shadow-lg: 0 20px 45px rgba(20, 20, 40, 0.12); 52 | --shadow-md: 0 8px 16px hsla(var(--primary-hue), var(--primary-sat), var(--primary-lit), 0.2); 53 | --shadow-focus: 0 0 0 3px hsla(var(--primary-hue), var(--primary-sat), var(--primary-lit), 0.12); 54 | 55 | /* 过渡动画 */ 56 | --transition-fast: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); 57 | } 58 | 59 | * { 60 | margin: 0; 61 | padding: 0; 62 | box-sizing: border-box; 63 | font-family: var(--font-sans); 64 | } 65 | 66 | body { 67 | background: var(--bg-page); 68 | min-width: 360px; 69 | max-width: 380px; 70 | padding: 12px; 71 | color: var(--text-strong); 72 | } 73 | 74 | .container { 75 | background: var(--bg-container); 76 | padding: 12px; 77 | border-radius: var(--radius-lg); 78 | box-shadow: var(--shadow-lg); 79 | display: flex; 80 | flex-direction: column; 81 | gap: 10px; 82 | } 83 | 84 | .hero { 85 | text-align: center; 86 | display: flex; 87 | flex-direction: column; 88 | gap: 4px; 89 | padding: 12px 0; 90 | background: linear-gradient(135deg, var(--primary-color-faded) 0%, var(--accent-blue-faded) 100%); 91 | border-radius: var(--radius-lg); 92 | margin: -12px -12px 0 -12px; 93 | position: relative; 94 | overflow: hidden; 95 | } 96 | 97 | .hero::before { 98 | content: ''; 99 | position: absolute; 100 | top: 0; 101 | left: 0; 102 | right: 0; 103 | bottom: 0; 104 | background: radial-gradient(circle at 70% 30%, var(--primary-color-faded-more) 0%, transparent 50%); 105 | pointer-events: none; 106 | } 107 | 108 | .hero-tag { 109 | align-self: center; 110 | padding: 4px 12px; 111 | border-radius: 999px; 112 | background: rgba(255, 255, 255, 0.9); 113 | color: var(--primary-color); 114 | font-size: 12px; 115 | font-weight: 600; 116 | letter-spacing: 0.05em; 117 | text-transform: uppercase; 118 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 119 | z-index: 1; 120 | } 121 | 122 | h1 { 123 | font-size: 20px; 124 | font-weight: 800; 125 | color: var(--text-strong); 126 | margin-top: 4px; 127 | z-index: 1; 128 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 129 | } 130 | 131 | .hero-subtitle { 132 | font-size: 12px; 133 | color: var(--text-muted); 134 | font-weight: 500; 135 | z-index: 1; 136 | margin-top: 2px; 137 | } 138 | 139 | .card { 140 | background: var(--bg-card); 141 | border-radius: var(--radius-md); 142 | border: 1px solid var(--border-color-light); 143 | padding: 10px; 144 | display: flex; 145 | flex-direction: column; 146 | gap: 8px; 147 | transition: var(--transition-fast); 148 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); 149 | } 150 | 151 | .card:hover { 152 | border-color: var(--border-color); 153 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 154 | transform: translateY(-1px); 155 | } 156 | 157 | .card-header { 158 | display: flex; 159 | align-items: center; 160 | gap: 10px; 161 | margin-bottom: 2px; 162 | } 163 | 164 | .card-icon { 165 | width: 28px; 166 | height: 28px; 167 | border-radius: var(--radius-sm); 168 | background: linear-gradient(135deg, var(--primary-color-faded), rgba(4, 96, 247, 0.08)); 169 | color: var(--primary-color); 170 | display: flex; 171 | align-items: center; 172 | justify-content: center; 173 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 174 | transition: var(--transition-fast); 175 | } 176 | 177 | .card:hover .card-icon { 178 | transform: scale(1.05); 179 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); 180 | } 181 | 182 | .card-icon svg { 183 | width: 16px; 184 | height: 16px; 185 | stroke-width: 2.5; 186 | } 187 | 188 | h2 { 189 | font-size: 14px; 190 | font-weight: 600; 191 | color: var(--text-normal); 192 | } 193 | 194 | .form-block { 195 | display: flex; 196 | flex-direction: column; 197 | gap: 3px; 198 | font-size: 12px; 199 | } 200 | 201 | .label { 202 | font-weight: 600; 203 | color: var(--text-normal); 204 | font-size: 12px; 205 | } 206 | 207 | input[type="text"], 208 | input[type="password"] { 209 | width: 100%; 210 | padding: 6px 8px; 211 | border: 1.5px solid var(--border-color); 212 | border-radius: var(--radius-sm); 213 | background: var(--bg-input); 214 | font-size: 12px; 215 | color: var(--text-strong); 216 | transition: var(--transition-fast); 217 | } 218 | 219 | input:focus { 220 | outline: none; 221 | border-color: var(--border-color-focus); 222 | box-shadow: var(--shadow-focus); 223 | } 224 | 225 | .input-with-btn { 226 | display: flex; 227 | gap: 6px; 228 | align-items: stretch; 229 | } 230 | 231 | .input-with-btn .input-modern { 232 | flex: 1; 233 | } 234 | 235 | .btn-group { 236 | display: flex; 237 | gap: 4px; 238 | align-items: center; 239 | } 240 | 241 | .btn-icon { 242 | border: none; 243 | border-radius: var(--radius-sm); 244 | background: var(--bg-tonal); 245 | padding: 8px 12px; 246 | font-size: 16px; 247 | cursor: pointer; 248 | transition: var(--transition-fast); 249 | color: var(--text-muted); 250 | display: flex; 251 | align-items: center; 252 | justify-content: center; 253 | } 254 | 255 | .btn-icon:hover { 256 | background: var(--bg-tonal-hover); 257 | color: var(--text-strong); 258 | } 259 | 260 | .readonly-pill { 261 | padding: 6px 10px; 262 | border-radius: var(--radius-md); 263 | background: var(--accent-blue-faded); 264 | color: var(--accent-blue); 265 | font-weight: 600; 266 | font-size: 12px; 267 | } 268 | 269 | small { 270 | font-size: 12px; 271 | color: var(--text-subtle); 272 | } 273 | 274 | .preview { 275 | background: var(--bg-container); 276 | border-radius: var(--radius-md); 277 | padding: 12px; 278 | border: 1.5px solid var(--primary-color-faded); 279 | display: flex; 280 | flex-direction: column; 281 | gap: 6px; 282 | } 283 | 284 | .preview strong { 285 | color: var(--text-strong); 286 | font-size: 13px; 287 | } 288 | 289 | .preview span { 290 | color: var(--primary-color); 291 | font-weight: 600; 292 | font-size: 15px; 293 | } 294 | 295 | .preview-card { 296 | background: linear-gradient(145deg, #ffffff, #f8f9fc); 297 | border-radius: var(--radius-md); 298 | padding: 8px; 299 | border: 1.5px solid var(--primary-color-faded); 300 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 301 | display: flex; 302 | transition: var(--transition-fast); 303 | position: relative; 304 | overflow: hidden; 305 | } 306 | 307 | .preview-card::before { 308 | content: ''; 309 | position: absolute; 310 | top: 0; 311 | left: 0; 312 | right: 0; 313 | height: 2px; 314 | background: linear-gradient(90deg, var(--primary-color), var(--accent-blue)); 315 | opacity: 0; 316 | transition: opacity 0.3s; 317 | } 318 | 319 | .preview-card:hover { 320 | border-color: var(--primary-color); 321 | box-shadow: 0 4px 12px rgba(244, 88, 5, 0.15); 322 | transform: translateY(-1px); 323 | } 324 | 325 | .preview-card:hover::before { 326 | opacity: 1; 327 | } 328 | 329 | .preview-content { 330 | display: flex; 331 | justify-content: space-between; 332 | align-items: center; 333 | gap: 12px; 334 | width: 100%; 335 | } 336 | 337 | .preview-content span { 338 | color: var(--primary-color); 339 | font-weight: 700; 340 | font-size: 13px; 341 | font-family: var(--font-mono); 342 | background: var(--primary-color-faded-more); 343 | padding: 4px 8px; 344 | border-radius: var(--radius-sm); 345 | flex: 1; 346 | word-break: break-all; 347 | } 348 | 349 | .copy-btn { 350 | border: none; 351 | border-radius: var(--radius-sm); 352 | background: linear-gradient(135deg, var(--accent-blue), #0253d9); 353 | color: white; 354 | padding: 6px 8px; 355 | cursor: pointer; 356 | transition: var(--transition-fast); 357 | display: flex; 358 | align-items: center; 359 | justify-content: center; 360 | box-shadow: 0 2px 6px rgba(4, 96, 247, 0.3); 361 | } 362 | 363 | .copy-btn:hover { 364 | background: linear-gradient(135deg, #0253d9, var(--accent-blue)); 365 | box-shadow: 0 4px 12px rgba(4, 96, 247, 0.4); 366 | transform: translateY(-1px) scale(1.05); 367 | } 368 | 369 | .copy-btn:active { 370 | transform: translateY(0) scale(1); 371 | } 372 | 373 | .copy-btn svg { 374 | width: 14px; 375 | height: 14px; 376 | stroke-width: 2.5; 377 | } 378 | 379 | .actions { 380 | gap: 6px; 381 | } 382 | 383 | .btn-row { 384 | display: flex; 385 | gap: 4px; 386 | } 387 | 388 | .btn-compact { 389 | flex: 1; 390 | padding: 8px 6px !important; 391 | font-size: 12px !important; 392 | min-width: 0; 393 | } 394 | 395 | .btn-compact .btn-icon-svg { 396 | width: 14px; 397 | height: 14px; 398 | } 399 | 400 | .btn-compact .btn-text { 401 | font-size: 11px; 402 | } 403 | 404 | .btn { 405 | border: none; 406 | border-radius: var(--radius-md); 407 | padding: 10px 12px; 408 | font-size: 13px; 409 | font-weight: 600; 410 | cursor: pointer; 411 | transition: var(--transition-fast); 412 | width: 100%; 413 | display: flex; 414 | align-items: center; 415 | justify-content: center; 416 | gap: 6px; 417 | position: relative; 418 | overflow: hidden; 419 | } 420 | 421 | .btn:hover { 422 | transform: translateY(-2px); 423 | } 424 | 425 | .btn:active { 426 | transform: translateY(-1px); 427 | } 428 | 429 | .btn-primary { 430 | background: linear-gradient(135deg, var(--primary-color), var(--primary-color-light)); 431 | color: var(--text-on-primary); 432 | box-shadow: var(--shadow-md); 433 | position: relative; 434 | overflow: hidden; 435 | } 436 | 437 | .btn-primary::before { 438 | content: ''; 439 | position: absolute; 440 | top: 0; 441 | left: -100%; 442 | width: 100%; 443 | height: 100%; 444 | background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); 445 | transition: left 0.5s; 446 | } 447 | 448 | .btn-primary:hover::before { 449 | left: 100%; 450 | } 451 | 452 | .btn-primary:hover { 453 | box-shadow: 0 12px 24px hsla(var(--primary-hue), var(--primary-sat), var(--primary-lit), 0.3); 454 | transform: translateY(-2px); 455 | } 456 | 457 | .btn-primary .btn-icon-svg { 458 | filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)); 459 | } 460 | 461 | .btn-tonal { 462 | background: var(--bg-tonal); 463 | color: var(--text-strong); 464 | border: 1px solid var(--border-color-light); 465 | } 466 | .btn-tonal:hover { 467 | background: var(--bg-tonal-hover); 468 | border-color: var(--border-color); 469 | } 470 | 471 | .btn-icon { 472 | font-size: 16px; 473 | line-height: 1; 474 | } 475 | 476 | .btn-icon-svg { 477 | width: 16px; 478 | height: 16px; 479 | flex-shrink: 0; 480 | stroke-width: 2.5; 481 | } 482 | 483 | .btn-text { 484 | line-height: 1; 485 | } 486 | 487 | .btn-ripple { 488 | position: absolute; 489 | top: 50%; 490 | left: 50%; 491 | width: 0; 492 | height: 0; 493 | border-radius: 50%; 494 | background: rgba(255, 255, 255, 0.3); 495 | transform: translate(-50%, -50%); 496 | transition: width 0.6s, height 0.6s; 497 | } 498 | 499 | .btn-primary:active .btn-ripple { 500 | width: 300px; 501 | height: 300px; 502 | } 503 | 504 | .input-modern { 505 | background: linear-gradient(145deg, #ffffff, #f8f9fc); 506 | border: 2px solid var(--border-color); 507 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); 508 | } 509 | 510 | .input-modern:focus { 511 | border-color: var(--primary-color); 512 | box-shadow: 513 | inset 0 1px 3px rgba(0, 0, 0, 0.1), 514 | 0 0 0 3px var(--primary-color-faded); 515 | } 516 | 517 | .btn-icon-modern { 518 | border: none; 519 | border-radius: var(--radius-sm); 520 | background: linear-gradient(145deg, var(--bg-tonal), #e8ebf2); 521 | padding: 6px 8px; 522 | font-size: 12px; 523 | cursor: pointer; 524 | transition: var(--transition-fast); 525 | color: var(--text-muted); 526 | display: flex; 527 | align-items: center; 528 | justify-content: center; 529 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 530 | border: 1px solid transparent; 531 | } 532 | 533 | .btn-icon-modern:hover { 534 | background: linear-gradient(145deg, var(--bg-tonal-hover), #d1d5e1); 535 | color: var(--primary-color); 536 | border-color: var(--primary-color-faded); 537 | box-shadow: 0 2px 8px rgba(244, 88, 5, 0.15); 538 | transform: translateY(-1px) scale(1.05); 539 | } 540 | 541 | .btn-icon-modern:active { 542 | transform: translateY(0) scale(1); 543 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 544 | } 545 | 546 | .btn-icon-modern svg { 547 | width: 14px; 548 | height: 14px; 549 | stroke-width: 2.5; 550 | transition: var(--transition-fast); 551 | } 552 | 553 | .status-card { 554 | background: linear-gradient(145deg, var(--bg-card), #f9fafb); 555 | } 556 | 557 | .status-icon { 558 | background: linear-gradient(135deg, var(--accent-blue), #0253d9); 559 | color: white; 560 | box-shadow: 0 2px 8px rgba(4, 96, 247, 0.3); 561 | } 562 | 563 | .help-icon { 564 | background: linear-gradient(135deg, #8b5cf6, #7c3aed); 565 | color: white; 566 | box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3); 567 | } 568 | 569 | .status-modern { 570 | padding: 8px 10px; 571 | border-radius: var(--radius-md); 572 | border: 2px solid var(--border-color); 573 | background: linear-gradient(145deg, #ffffff, #f8f9fc); 574 | font-size: 12px; 575 | font-weight: 500; 576 | color: var(--text-strong); 577 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); 578 | transition: var(--transition-fast); 579 | } 580 | 581 | .status-modern.success { 582 | border-color: var(--status-success); 583 | background: linear-gradient(145deg, #f0fdf4, #dcfce7); 584 | color: var(--status-success); 585 | font-weight: 600; 586 | } 587 | 588 | .status-modern.error { 589 | border-color: var(--status-error); 590 | background: linear-gradient(145deg, #fef2f2, #fecaca); 591 | color: var(--status-error); 592 | font-weight: 600; 593 | } 594 | 595 | .status-modern.running { 596 | border-color: var(--status-running); 597 | background: linear-gradient(145deg, #fffbeb, #fef3c7); 598 | color: #b46a00; 599 | font-weight: 600; 600 | } 601 | 602 | .log-container { 603 | background: var(--bg-log); 604 | border-radius: var(--radius-md); 605 | overflow: hidden; 606 | box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3); 607 | } 608 | 609 | .log-header { 610 | display: flex; 611 | justify-content: space-between; 612 | align-items: center; 613 | padding: 8px 12px; 614 | background: rgba(30, 41, 59, 0.8); 615 | color: #e2e8f0; 616 | font-size: 12px; 617 | font-weight: 500; 618 | border-bottom: 1px solid rgba(148, 163, 184, 0.2); 619 | } 620 | 621 | .btn-icon-small { 622 | border: none; 623 | border-radius: var(--radius-sm); 624 | background: rgba(255, 255, 255, 0.1); 625 | color: #e2e8f0; 626 | padding: 4px 6px; 627 | cursor: pointer; 628 | transition: var(--transition-fast); 629 | display: flex; 630 | align-items: center; 631 | justify-content: center; 632 | } 633 | 634 | .btn-icon-small:hover { 635 | background: rgba(255, 255, 255, 0.2); 636 | color: white; 637 | } 638 | 639 | .btn-icon-small svg { 640 | width: 14px; 641 | height: 14px; 642 | } 643 | 644 | .log-modern { 645 | color: #e2e8f0; 646 | font-family: var(--font-mono); 647 | font-size: 11px; 648 | padding: 8px; 649 | max-height: 120px; 650 | overflow-y: auto; 651 | background: transparent; 652 | } 653 | 654 | .log-entry { 655 | margin-bottom: 6px; 656 | padding: 4px 8px; 657 | border-radius: var(--radius-sm); 658 | background: rgba(255, 255, 255, 0.05); 659 | line-height: 1.4; 660 | } 661 | .log-entry:last-child { margin-bottom: 0; } 662 | .log-entry.info { color: #7dd3fc; background: rgba(125, 211, 252, 0.1); } 663 | .log-entry.success { color: #bef264; background: rgba(190, 242, 100, 0.1); } 664 | .log-entry.error { color: #fca5a5; background: rgba(252, 165, 165, 0.1); } 665 | 666 | .tips-card-modern { 667 | display: flex; 668 | align-items: flex-start; 669 | gap: 10px; 670 | padding: 12px; 671 | background: linear-gradient(135deg, var(--accent-blue-faded), rgba(4, 96, 247, 0.15); 672 | border-radius: var(--radius-md); 673 | border: 1px solid var(--accent-blue); 674 | font-size: 12px; 675 | color: var(--accent-blue); 676 | } 677 | 678 | .tips-icon { 679 | font-size: 16px; 680 | line-height: 1; 681 | flex-shrink: 0; 682 | margin-top: 2px; 683 | } 684 | 685 | .tips-content { 686 | flex: 1; 687 | line-height: 1.5; 688 | } 689 | 690 | .tips-content p { 691 | margin: 0; 692 | } 693 | 694 | .help-modern { 695 | background: linear-gradient(145deg, var(--bg-card), #fafbfc); 696 | } 697 | 698 | .help-icon { 699 | background: linear-gradient(135deg, #8b5cf6, #7c3aed); 700 | color: white; 701 | } 702 | 703 | .help-content { 704 | padding: 8px 0; 705 | } 706 | 707 | .help-list { 708 | list-style: none; 709 | margin: 0; 710 | padding: 0; 711 | counter-reset: help-counter; 712 | } 713 | 714 | .help-item { 715 | display: flex; 716 | align-items: flex-start; 717 | gap: 8px; 718 | margin-bottom: 8px; 719 | padding: 8px; 720 | background: linear-gradient(145deg, #ffffff, #f8fafc); 721 | border-radius: var(--radius-md); 722 | border: 1px solid var(--border-color-light); 723 | transition: var(--transition-fast); 724 | } 725 | 726 | .help-item:hover { 727 | border-color: var(--primary-color); 728 | box-shadow: 0 4px 12px rgba(244, 88, 5, 0.1); 729 | transform: translateX(4px); 730 | } 731 | 732 | .help-item:last-child { 733 | margin-bottom: 0; 734 | } 735 | 736 | .help-number { 737 | display: flex; 738 | align-items: center; 739 | justify-content: center; 740 | width: 20px; 741 | height: 20px; 742 | border-radius: 50%; 743 | background: linear-gradient(135deg, var(--primary-color), var(--primary-color-light)); 744 | color: white; 745 | font-size: 11px; 746 | font-weight: 700; 747 | flex-shrink: 0; 748 | } 749 | 750 | .help-text { 751 | flex: 1; 752 | font-size: 12px; 753 | line-height: 1.5; 754 | color: var(--text-muted); 755 | } 756 | 757 | .notice-modern { 758 | border: 2px solid var(--status-running); 759 | background: linear-gradient(145deg, #fffbeb, #fef3c7); 760 | } 761 | 762 | .notice-header { 763 | display: flex; 764 | align-items: center; 765 | gap: 10px; 766 | margin-bottom: 12px; 767 | } 768 | 769 | .notice-icon { 770 | font-size: 20px; 771 | line-height: 1; 772 | } 773 | 774 | .notice-title { 775 | color: #b45309; 776 | font-size: 16px; 777 | font-weight: 700; 778 | } 779 | 780 | .notice-message { 781 | font-size: 14px; 782 | line-height: 1.5; 783 | color: #b45309; 784 | padding: 12px; 785 | background: rgba(255, 255, 255, 0.5); 786 | border-radius: var(--radius-sm); 787 | margin-bottom: 16px; 788 | } 789 | 790 | .notice-links { 791 | display: flex; 792 | justify-content: space-between; 793 | gap: 8px; 794 | font-size: 12px; 795 | } 796 | 797 | .notice-link { 798 | flex: 1; 799 | text-align: center; 800 | text-decoration: none; 801 | font-weight: 600; 802 | padding: 8px 12px; 803 | border-radius: var(--radius-md); 804 | transition: var(--transition-fast); 805 | } 806 | 807 | .notice-link-primary { 808 | background: var(--primary-color); 809 | color: white; 810 | } 811 | 812 | .notice-link-primary:hover { 813 | background: var(--primary-color-light); 814 | transform: translateY(-2px); 815 | box-shadow: 0 4px 12px rgba(244, 88, 5, 0.3); 816 | } 817 | 818 | .notice-link-secondary { 819 | background: var(--bg-tonal); 820 | color: var(--text-strong); 821 | border: 1px solid var(--border-color); 822 | } 823 | 824 | .notice-link-secondary:hover { 825 | background: var(--bg-tonal-hover); 826 | transform: translateY(-1px); 827 | } 828 | 829 | .notice-link-tertiary { 830 | background: transparent; 831 | color: var(--text-muted); 832 | border: 1px solid var(--border-color); 833 | } 834 | 835 | .notice-link-tertiary:hover { 836 | background: var(--bg-tonal); 837 | color: var(--text-strong); 838 | } 839 | 840 | -------------------------------------------------------------------------------- /popup-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具函数库 3 | * 包含去混淆后的核心工具函数 4 | */ 5 | 6 | const MOEMAIL_DOMAIN = 'moemail.app'; 7 | 8 | // 全局 elements 对象(将在初始化时填充) 9 | let elements = {}; 10 | let currentMoemailAccount = null; 11 | 12 | // 默认配置(使用函数来延迟初始化,避免在函数定义之前调用) 13 | function getDefaultConfig() { 14 | return { 15 | 'emailPrefix': generateRandomEmailPrefix(), 16 | 'password': generateRandomPassword() 17 | }; 18 | } 19 | 20 | // 为了兼容性,也导出一个对象(在函数定义后初始化) 21 | let DEFAULT_CONFIG = null; 22 | 23 | /** 24 | * 生成随机密码 25 | * @param {number} length - 密码长度,默认12-16位 26 | * @returns {string} 随机密码 27 | */ 28 | function generateRandomPassword(length = null) { 29 | const lowercase = 'abcdefghijklmnopqrstuvwxyz'; 30 | const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 31 | const numbers = '0123456789'; 32 | const special = '!@#$%^&*'; 33 | 34 | const allChars = lowercase + uppercase + numbers + special; 35 | let password = ''; 36 | 37 | // 随机长度:12-16位 38 | const passwordLength = length || Math.floor(Math.random() * 5) + 12; 39 | 40 | // 确保至少包含每种类型的字符 41 | password += lowercase[Math.floor(Math.random() * lowercase.length)]; 42 | password += uppercase[Math.floor(Math.random() * uppercase.length)]; 43 | password += numbers[Math.floor(Math.random() * numbers.length)]; 44 | password += special[Math.floor(Math.random() * special.length)]; 45 | 46 | // 填充剩余字符 47 | for (let i = password.length; i < passwordLength; i++) { 48 | password += allChars[Math.floor(Math.random() * allChars.length)]; 49 | } 50 | 51 | // 打乱字符顺序 52 | return password.split('').sort(() => Math.random() - 0.5).join(''); 53 | } 54 | 55 | /** 56 | * 生成随机字母数字字符串 57 | * @param {number} length - 字符串长度 58 | * @returns {string} 随机字符串 59 | */ 60 | function generateRandomAlphaNumeric(length = 8) { 61 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 62 | let result = ''; 63 | 64 | for (let i = 0; i < length; i++) { 65 | result += chars[Math.floor(Math.random() * chars.length)]; 66 | } 67 | 68 | return result; 69 | } 70 | 71 | /** 72 | * 生成随机邮箱前缀(大小写字母 + 数字,长度随机) 73 | * @returns {string} 随机邮箱前缀 74 | */ 75 | function generateRandomEmailPrefix() { 76 | const length = Math.floor(Math.random() * 5) + 12; // 8-12 位 77 | return generateRandomAlphaNumeric(length); 78 | } 79 | 80 | /** 81 | * 与后台通信的通用封装 82 | * @param {string} type - 消息类型 83 | * @param {Object} payload - 消息数据 84 | * @returns {Promise} 85 | */ 86 | function sendBackgroundMessage(type, payload = {}) { 87 | return new Promise((resolve, reject) => { 88 | if (!chrome?.runtime?.sendMessage) { 89 | reject(new Error('后台通信不可用')); 90 | return; 91 | } 92 | 93 | chrome.runtime.sendMessage({ type, payload }, (response) => { 94 | if (chrome.runtime.lastError) { 95 | reject(new Error(chrome.runtime.lastError.message)); 96 | return; 97 | } 98 | resolve(response); 99 | }); 100 | }); 101 | } 102 | 103 | /** 104 | * 使用指定邮箱更新 UI 显示 105 | * @param {string} email - 完整邮箱 106 | */ 107 | function applyMoemailEmailToUI(email) { 108 | if (!elements || !email) { 109 | return; 110 | } 111 | 112 | const localPart = email.split('@')[0] || ''; 113 | 114 | if (elements.emailPrefix) { 115 | elements.emailPrefix.value = localPart; 116 | } 117 | if (elements.emailPreview) { 118 | elements.emailPreview.textContent = email; 119 | } 120 | 121 | try { 122 | updateEmailPreview(); 123 | } catch (error) { 124 | console.warn('[工具] 更新邮箱预览失败:', error); 125 | } 126 | } 127 | 128 | /** 129 | * 创建 Moemail 邮箱并更新 UI 130 | * @param {string} nameHint - 可选的名称提示 131 | * @returns {Promise<{email: string, id: string, raw: any}>} 132 | */ 133 | async function createMoemailAccount(nameHint = '') { 134 | const fallbackName = nameHint && nameHint.trim() ? nameHint.trim() : generateRandomEmailPrefix(); 135 | 136 | addLog('正在创建 Moemail 邮箱...', 'info'); 137 | updateStatus('正在创建临时邮箱...', 'running'); 138 | 139 | const response = await sendBackgroundMessage('moemailCreate', { name: fallbackName }); 140 | 141 | if (!response?.success || !response.data) { 142 | throw new Error(response?.error || 'Moemail 创建失败'); 143 | } 144 | 145 | currentMoemailAccount = response.data; 146 | applyMoemailEmailToUI(currentMoemailAccount.email); 147 | updateStatus('Moemail 邮箱已创建', 'success'); 148 | addLog(`Moemail 邮箱已创建:${currentMoemailAccount.email}`, 'success'); 149 | 150 | return currentMoemailAccount; 151 | } 152 | 153 | /** 154 | * 更新状态显示 155 | * @param {string} message - 状态消息 156 | * @param {string} type - 状态类型:'success', 'error', 'running', 默认'info' 157 | */ 158 | function updateStatus(message, type = 'info') { 159 | if (!elements || !elements.status) { 160 | console.warn('[工具] elements.status 未初始化'); 161 | return; 162 | } 163 | 164 | try { 165 | elements.status.textContent = message; 166 | elements.status.className = 'status ' + type; 167 | } catch (error) { 168 | console.error('[工具] 更新状态失败:', error); 169 | } 170 | } 171 | 172 | /** 173 | * 添加日志 174 | * @param {string} message - 日志消息 175 | * @param {string} type - 日志类型:'info', 'success', 'error', 默认'info' 176 | */ 177 | function addLog(message, type = 'info') { 178 | if (!elements || !elements.log) { 179 | console.warn('[工具] elements.log 未初始化'); 180 | return; 181 | } 182 | 183 | try { 184 | const logEntry = document.createElement('div'); 185 | logEntry.className = 'log-entry ' + type; 186 | 187 | const timestamp = new Date().toLocaleTimeString('zh-CN', { 188 | hour12: false, 189 | hour: '2-digit', 190 | minute: '2-digit', 191 | second: '2-digit' 192 | }); 193 | 194 | logEntry.textContent = `[${timestamp}] ${message}`; 195 | elements.log.appendChild(logEntry); 196 | 197 | // 自动滚动到底部 198 | elements.log.scrollTop = elements.log.scrollHeight; 199 | 200 | // 限制日志条数,避免内存泄漏 201 | const maxLogs = 100; 202 | const logEntries = elements.log.querySelectorAll('.log-entry'); 203 | if (logEntries.length > maxLogs) { 204 | logEntries[0].remove(); 205 | } 206 | } catch (error) { 207 | console.error('[工具] 添加日志失败:', error); 208 | } 209 | } 210 | 211 | /** 212 | * 获取当前邮箱域名 213 | * @returns {string} 邮箱域名 214 | */ 215 | function getCurrentDomain() { 216 | return MOEMAIL_DOMAIN; 217 | } 218 | 219 | /** 220 | * 更新邮箱预览 221 | */ 222 | function updateEmailPreview() { 223 | if (!elements || !elements.emailPreview) { 224 | return; 225 | } 226 | 227 | try { 228 | const prefix = elements.emailPrefix?.value || 'windsurf'; 229 | const domain = getCurrentDomain(); 230 | 231 | if (domain !== MOEMAIL_DOMAIN) { 232 | currentMoemailAccount = null; 233 | } 234 | 235 | const email = `${prefix}@${domain}`; 236 | elements.emailPreview.textContent = email; 237 | } catch (error) { 238 | console.error('[工具] 更新邮箱预览失败:', error); 239 | } 240 | } 241 | 242 | /** 243 | * 切换自定义域名输入框显示 244 | */ 245 | /** 246 | * 切换密码显示/隐藏 247 | */ 248 | function togglePasswordVisibility() { 249 | if (!elements || !elements.password || !elements.togglePassword) { 250 | return; 251 | } 252 | 253 | try { 254 | const isPassword = elements.password.type === 'password'; 255 | elements.password.type = isPassword ? 'text' : 'password'; 256 | elements.togglePassword.textContent = isPassword ? '🙈' : '👁️'; 257 | } catch (error) { 258 | console.error('[工具] 切换密码显示失败:', error); 259 | } 260 | } 261 | 262 | /** 263 | * 生成新密码并更新到输入框 264 | */ 265 | function generateNewPassword() { 266 | try { 267 | const newPassword = generateRandomPassword(); 268 | 269 | if (elements && elements.password) { 270 | elements.password.value = newPassword; 271 | // 确保密码类型正确 272 | if (elements.password.type === 'text') { 273 | elements.password.type = 'password'; 274 | if (elements.togglePassword) { 275 | elements.togglePassword.textContent = '👁️'; 276 | } 277 | } 278 | } 279 | 280 | updateStatus('密码已生成', 'success'); 281 | addLog('新密码已生成: ' + newPassword, 'success'); 282 | 283 | // 2秒后清除状态 284 | setTimeout(() => { 285 | updateStatus('等待开始...', 'info'); 286 | }, 2000); 287 | } catch (error) { 288 | console.error('[工具] 生成新密码失败:', error); 289 | updateStatus('生成密码失败', 'error'); 290 | } 291 | } 292 | 293 | /** 294 | * 生成新邮箱前缀并更新到输入框 295 | */ 296 | function generateNewEmailPrefix() { 297 | try { 298 | const newPrefix = generateRandomEmailPrefix(); 299 | 300 | if (elements && elements.emailPrefix) { 301 | elements.emailPrefix.value = newPrefix; 302 | } 303 | 304 | updateEmailPreview(); 305 | updateStatus('邮箱前缀已生成', 'success'); 306 | addLog('新邮箱前缀已生成: ' + newPrefix, 'success'); 307 | 308 | // 2秒后清除状态 309 | setTimeout(() => { 310 | updateStatus('等待开始...', 'info'); 311 | }, 2000); 312 | } catch (error) { 313 | console.error('[工具] 生成新邮箱前缀失败:', error); 314 | updateStatus('生成邮箱前缀失败', 'error'); 315 | } 316 | } 317 | 318 | /** 319 | * 加载配置 320 | */ 321 | async function loadConfig() { 322 | try { 323 | const savedConfig = await chrome.storage.local.get(DEFAULT_CONFIG); 324 | const config = { ...DEFAULT_CONFIG, ...savedConfig }; 325 | 326 | // 如果没有保存的邮箱前缀,生成一个新的 327 | if (!config.emailPrefix) { 328 | config.emailPrefix = generateRandomEmailPrefix(); 329 | } 330 | 331 | // 更新UI元素 332 | if (elements) { 333 | if (elements.emailPrefix) { 334 | elements.emailPrefix.value = config.emailPrefix || generateRandomEmailPrefix(); 335 | } 336 | if (elements.password) { 337 | elements.password.value = config.password || generateRandomPassword(); 338 | } 339 | } 340 | 341 | updateEmailPreview(); 342 | 343 | addLog('配置已加载', 'success'); 344 | } catch (error) { 345 | console.error('[工具] 加载配置失败:', error); 346 | addLog('加载配置失败: ' + error.message, 'error'); 347 | } 348 | } 349 | 350 | /** 351 | * 保存配置 352 | */ 353 | async function saveConfig() { 354 | try { 355 | if (!elements) { 356 | throw new Error('元素未初始化'); 357 | } 358 | 359 | const config = { 360 | emailPrefix: elements.emailPrefix?.value || '', 361 | password: elements.password?.value || '' 362 | }; 363 | 364 | // 同时保存API Key 365 | const apiKeyInput = document.getElementById('moemailApiKey'); 366 | if (apiKeyInput && apiKeyInput.value) { 367 | try { 368 | if (typeof ApiKeyManager !== 'undefined' && ApiKeyManager.saveMoemailApiKey) { 369 | await ApiKeyManager.saveMoemailApiKey(apiKeyInput.value); 370 | } 371 | } catch (error) { 372 | console.warn('[工具] 保存API Key失败:', error); 373 | } 374 | } 375 | 376 | await chrome.storage.local.set(config); 377 | updateStatus('配置已保存', 'success'); 378 | addLog('配置已保存', 'success'); 379 | } catch (error) { 380 | console.error('[工具] 保存配置失败:', error); 381 | updateStatus('保存配置失败: ' + error.message, 'error'); 382 | addLog('保存配置失败: ' + error.message, 'error'); 383 | } 384 | } 385 | 386 | /** 387 | * 绑定事件监听器 388 | */ 389 | function bindEventListeners() { 390 | if (!elements) { 391 | console.error('[工具] elements 未初始化'); 392 | return; 393 | } 394 | 395 | try { 396 | // 邮箱前缀输入框 - 更新预览 397 | if (elements.emailPrefix) { 398 | elements.emailPrefix.addEventListener('input', updateEmailPreview); 399 | } 400 | 401 | // 密码显示/隐藏切换按钮 402 | if (elements.togglePassword) { 403 | elements.togglePassword.addEventListener('click', togglePasswordVisibility); 404 | } 405 | 406 | // 生成新密码按钮 407 | if (elements.generatePassword) { 408 | elements.generatePassword.addEventListener('click', generateNewPassword); 409 | } 410 | 411 | // 生成新邮箱前缀按钮 412 | if (elements.generatePrefix) { 413 | elements.generatePrefix.addEventListener('click', generateNewEmailPrefix); 414 | } 415 | 416 | // 打开注册页面按钮 417 | if (elements.openPageBtn) { 418 | elements.openPageBtn.addEventListener('click', openRegistrationPage); 419 | } 420 | 421 | // 保存配置按钮 422 | if (elements.saveConfigBtn) { 423 | elements.saveConfigBtn.addEventListener('click', saveConfig); 424 | } 425 | 426 | // 导出账号按钮 427 | if (elements.exportAccountsBtn) { 428 | elements.exportAccountsBtn.addEventListener('click', exportAccountsToDesktop); 429 | } 430 | 431 | // 开始注册按钮 432 | if (elements.startBtn) { 433 | elements.startBtn.addEventListener('click', startRegistration); 434 | } 435 | 436 | // 自动填卡按钮 437 | if (elements.autoFillCardBtn) { 438 | elements.autoFillCardBtn.addEventListener('click', autoFillCard); 439 | } 440 | 441 | // 复制邮箱按钮 442 | const copyBtn = document.querySelector('.copy-btn'); 443 | if (copyBtn) { 444 | copyBtn.addEventListener('click', copyEmailToClipboard); 445 | } 446 | 447 | // 生成邮箱密码按钮 448 | if (elements.generateEmailPassword) { 449 | elements.generateEmailPassword.addEventListener('click', generateNewEmailPassword); 450 | } 451 | 452 | // 测试API Key按钮 453 | if (elements.testApiKey) { 454 | elements.testApiKey.addEventListener('click', testMoemailApiKey); 455 | } 456 | 457 | // 清空日志按钮 458 | if (elements.clearLog) { 459 | elements.clearLog.addEventListener('click', clearLog); 460 | } 461 | 462 | // 使用说明折叠/展开 463 | const helpHeader = document.querySelector('.help-header'); 464 | if (helpHeader) { 465 | helpHeader.addEventListener('click', toggleHelp); 466 | } 467 | 468 | console.log('[工具] 事件监听器已绑定'); 469 | } catch (error) { 470 | console.error('[工具] 绑定事件监听器失败:', error); 471 | } 472 | } 473 | 474 | /** 475 | * 检查并打开注册页面 476 | * 如果当前标签页已经是注册页面,则直接使用;否则打开新标签页 477 | */ 478 | async function checkAndOpenRegistrationPage() { 479 | try { 480 | const queryOptions = { active: true, currentWindow: true }; 481 | const [currentTab] = await chrome.tabs.query(queryOptions); 482 | 483 | if (!currentTab || !currentTab.url) { 484 | updateStatus('无法获取当前标签页信息', 'error'); 485 | return; 486 | } 487 | 488 | const currentUrl = currentTab.url; 489 | 490 | // 检查是否是注册页面 491 | if (currentUrl.includes('windsurf.com/account/register') || 492 | currentUrl.includes('codeium.com/account/register')) { 493 | addLog('当前页面已是注册页面', 'success'); 494 | updateStatus('当前页面已是注册页面', 'success'); 495 | return; 496 | } 497 | 498 | // 检查是否是 Windsurf 或 Codeium 的其他页面 499 | if (currentUrl.includes('windsurf.com') || currentUrl.includes('codeium.com')) { 500 | addLog('正在打开注册页面...', 'info'); 501 | updateStatus('正在打开注册页面...', 'running'); 502 | await openRegistrationPage(); 503 | return; 504 | } 505 | 506 | // 其他页面,提示用户 507 | updateStatus('请先打开 Windsurf 或 Codeium 网站', 'error'); 508 | } catch (error) { 509 | console.error('[工具] 检查注册页面失败:', error); 510 | updateStatus('检查注册页面失败: ' + error.message, 'error'); 511 | } 512 | } 513 | 514 | /** 515 | * 打开注册页面 516 | */ 517 | async function openRegistrationPage() { 518 | try { 519 | addLog('正在打开注册页面...', 'info'); 520 | updateStatus('正在打开注册页面...', 'running'); 521 | 522 | const createOptions = { 523 | url: 'https://windsurf.com/account/register', 524 | active: true 525 | }; 526 | 527 | await chrome.tabs.create(createOptions); 528 | 529 | addLog('注册页面已打开', 'success'); 530 | updateStatus('注册页面已打开', 'success'); 531 | 532 | // 延迟后检查页面是否加载完成 533 | setTimeout(async () => { 534 | try { 535 | const queryOptions = { url: 'https://windsurf.com/account/register' }; 536 | const tabs = await chrome.tabs.query(queryOptions); 537 | 538 | if (tabs && tabs.length > 0) { 539 | const tab = tabs[0]; 540 | if (tab.status === 'complete') { 541 | updateStatus('页面已加载完成', 'success'); 542 | } else { 543 | // 监听标签页更新 544 | const listener = (tabId, changeInfo) => { 545 | if (tabId === tab.id && changeInfo.status === 'complete') { 546 | updateStatus('页面已加载完成', 'success'); 547 | chrome.tabs.onUpdated.removeListener(listener); 548 | } 549 | }; 550 | chrome.tabs.onUpdated.addListener(listener); 551 | } 552 | } 553 | } catch (error) { 554 | console.error('[工具] 检查页面加载状态失败:', error); 555 | } 556 | }, 1000); 557 | } catch (error) { 558 | console.error('[工具] 打开注册页面失败:', error); 559 | addLog('打开注册页面失败: ' + error.message, 'error'); 560 | updateStatus('打开注册页面失败: ' + error.message, 'error'); 561 | } 562 | } 563 | 564 | /** 565 | * 导出账号到桌面 566 | */ 567 | async function exportAccountsToDesktop() { 568 | try { 569 | addLog('正在导出账号...', 'info'); 570 | 571 | // 获取保存的账号列表 572 | const result = await chrome.storage.local.get(['savedAccounts']); 573 | const savedAccounts = result.savedAccounts || []; 574 | 575 | if (savedAccounts.length === 0) { 576 | addLog('没有可导出的账号', 'error'); 577 | updateStatus('没有可导出的账号', 'error'); 578 | return; 579 | } 580 | 581 | // 准备导出数据 582 | const exportData = { 583 | accounts: savedAccounts, 584 | exportTime: new Date().toISOString(), 585 | total: savedAccounts.length 586 | }; 587 | 588 | // 转换为 JSON 字符串 589 | const jsonContent = JSON.stringify(exportData, null, 2); 590 | 591 | // 创建 Blob 592 | const blob = new Blob([jsonContent], { type: 'application/json' }); 593 | const url = URL.createObjectURL(blob); 594 | 595 | // 生成文件名(包含时间戳) 596 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); 597 | const filename = `windsurf-accounts-${timestamp}.json`; 598 | 599 | // 下载文件 600 | const downloadOptions = { 601 | url: url, 602 | filename: filename, 603 | saveAs: true 604 | }; 605 | 606 | chrome.downloads.download(downloadOptions, (downloadId) => { 607 | if (chrome.runtime.lastError) { 608 | const error = chrome.runtime.lastError.message; 609 | console.error('[工具] 下载失败:', error); 610 | addLog('导出失败: ' + error, 'error'); 611 | updateStatus('导出失败: ' + error, 'error'); 612 | URL.revokeObjectURL(url); 613 | } else { 614 | addLog(`成功导出 ${savedAccounts.length} 个账号`, 'success'); 615 | updateStatus(`成功导出 ${savedAccounts.length} 个账号`, 'success'); 616 | 617 | // 延迟清理 URL 618 | setTimeout(() => { 619 | URL.revokeObjectURL(url); 620 | }, 5000); 621 | } 622 | }); 623 | } catch (error) { 624 | console.error('[工具] 导出账号失败:', error); 625 | addLog('导出账号失败: ' + error.message, 'error'); 626 | updateStatus('导出账号失败: ' + error.message, 'error'); 627 | } 628 | } 629 | 630 | /** 631 | * 等待页面加载完成 632 | * @param {number} tabId - 标签页ID 633 | * @returns {Promise} 634 | */ 635 | async function waitForPageLoad(tabId) { 636 | return new Promise((resolve) => { 637 | const listener = (updatedTabId, changeInfo) => { 638 | if (updatedTabId === tabId && changeInfo.status === 'complete') { 639 | chrome.tabs.onUpdated.removeListener(listener); 640 | resolve(); 641 | } 642 | }; 643 | 644 | chrome.tabs.onUpdated.addListener(listener); 645 | 646 | // 检查页面是否已经加载完成 647 | chrome.tabs.get(tabId, (tab) => { 648 | if (tab && tab.status === 'complete') { 649 | chrome.tabs.onUpdated.removeListener(listener); 650 | resolve(); 651 | } 652 | }); 653 | }); 654 | } 655 | 656 | /** 657 | * 注入并执行脚本 658 | * @param {number} tabId - 标签页ID 659 | * @param {string} action - 要执行的操作 660 | * @param {Object} data - 要传递的数据 661 | * @returns {Promise} 662 | */ 663 | async function injectAndExecute(tabId, action, data = {}) { 664 | try { 665 | const message = { 666 | action: action, 667 | data: data 668 | }; 669 | 670 | // 先尝试发送消息(如果 content script 已加载) 671 | return new Promise((resolve, reject) => { 672 | chrome.tabs.sendMessage(tabId, message, (response) => { 673 | if (chrome.runtime.lastError) { 674 | // 如果消息发送失败,等待一下再重试(给 content script 时间加载) 675 | console.warn('[工具] 消息发送失败,等待 content script 加载:', chrome.runtime.lastError.message); 676 | 677 | setTimeout(() => { 678 | chrome.tabs.sendMessage(tabId, message, (retryResponse) => { 679 | if (chrome.runtime.lastError) { 680 | reject(new Error('无法连接到 content script: ' + chrome.runtime.lastError.message + '。请确保页面已完全加载。')); 681 | } else { 682 | resolve(retryResponse); 683 | } 684 | }); 685 | }, 1000); 686 | } else { 687 | resolve(response); 688 | } 689 | }); 690 | }); 691 | } catch (error) { 692 | console.error('[工具] 注入脚本失败:', error); 693 | throw error; 694 | } 695 | } 696 | 697 | /** 698 | * 开始自动注册流程 699 | */ 700 | async function startRegistration() { 701 | try { 702 | // 获取表单数据 703 | if (!elements) { 704 | updateStatus('元素未初始化', 'error'); 705 | return; 706 | } 707 | 708 | let emailPrefix = elements.emailPrefix?.value?.trim() || ''; 709 | const password = elements.password?.value || ''; 710 | const domain = getCurrentDomain(); 711 | 712 | // 验证输入 713 | if (!emailPrefix && domain !== MOEMAIL_DOMAIN) { 714 | updateStatus('邮箱前缀不能为空', 'error'); 715 | return; 716 | } 717 | 718 | if (!password || password.length < 8) { 719 | updateStatus('密码至少需要8位', 'error'); 720 | return; 721 | } 722 | 723 | // 获取当前标签页 724 | const queryOptions = { active: true, currentWindow: true }; 725 | const [currentTab] = await chrome.tabs.query(queryOptions); 726 | 727 | if (!currentTab) { 728 | updateStatus('无法获取当前标签页', 'error'); 729 | return; 730 | } 731 | 732 | // 检查是否是注册页面 733 | const currentUrl = currentTab.url || ''; 734 | const isRegisterPage = currentUrl.includes('windsurf.com/account/register') || 735 | currentUrl.includes('codeium.com/account/register'); 736 | 737 | let targetTab = currentTab; 738 | 739 | // 如果不是注册页面,打开注册页面 740 | if (!isRegisterPage) { 741 | addLog('当前页面不是注册页面,正在打开注册页面...', 'info'); 742 | await openRegistrationPage(); 743 | 744 | // 等待新标签页打开 745 | await new Promise(resolve => setTimeout(resolve, 1000)); 746 | 747 | // 获取新打开的标签页 748 | const tabs = await chrome.tabs.query({ url: 'https://windsurf.com/account/register' }); 749 | if (tabs && tabs.length > 0) { 750 | targetTab = tabs[0]; 751 | } 752 | } 753 | 754 | let moemailAccount = null; 755 | if (domain === MOEMAIL_DOMAIN) { 756 | try { 757 | moemailAccount = await createMoemailAccount(emailPrefix); 758 | emailPrefix = moemailAccount.email.split('@')[0] || emailPrefix; 759 | } catch (error) { 760 | console.error('[工具] 创建 Moemail 邮箱失败:', error); 761 | updateStatus('创建 Moemail 邮箱失败: ' + error.message, 'error'); 762 | return; 763 | } 764 | } 765 | 766 | // 保存配置(确保 UI 值已更新) 767 | await saveConfig(); 768 | 769 | // 构建完整邮箱地址 770 | const email = moemailAccount ? moemailAccount.email : `${emailPrefix}@${domain}`; 771 | 772 | // 更新状态 773 | updateStatus('正在开始注册...', 'running'); 774 | addLog('开始注册,邮箱: ' + email, 'info'); 775 | 776 | // 准备注册数据 777 | const registrationData = { 778 | email: email, 779 | password: password, 780 | moemailId: moemailAccount?.id || null 781 | }; 782 | 783 | // 等待页面加载完成 784 | await waitForPageLoad(targetTab.id); 785 | 786 | // 先显示浮动面板 787 | try { 788 | await injectAndExecute(targetTab.id, 'showFloatingPanel'); 789 | } catch (e) { 790 | console.warn('[工具] 无法显示浮动面板:', e); 791 | } 792 | 793 | // 向 content script 发送消息开始注册 794 | addLog('正在向页面发送注册指令...', 'info'); 795 | await injectAndExecute(targetTab.id, 'startRegistration', registrationData); 796 | 797 | addLog('注册流程已启动', 'success'); 798 | updateStatus('注册流程已启动', 'success'); 799 | 800 | // 更新邮箱序号(为下次注册做准备) 801 | if (!moemailAccount) { 802 | await saveConfig(); 803 | } 804 | } catch (error) { 805 | console.error('[工具] 开始注册失败:', error); 806 | addLog('开始注册失败: ' + error.message, 'error'); 807 | updateStatus('开始注册失败: ' + error.message, 'error'); 808 | } 809 | } 810 | 811 | /** 812 | * 复制邮箱到剪贴板 813 | */ 814 | async function copyEmailToClipboard() { 815 | try { 816 | const email = elements.emailPreview?.textContent || ''; 817 | if (!email) { 818 | updateStatus('没有可复制的邮箱', 'error'); 819 | return; 820 | } 821 | 822 | await navigator.clipboard.writeText(email); 823 | updateStatus('邮箱已复制到剪贴板', 'success'); 824 | addLog(`邮箱已复制: ${email}`, 'success'); 825 | 826 | // 2秒后清除状态 827 | setTimeout(() => { 828 | updateStatus('等待开始...', 'info'); 829 | }, 2000); 830 | } catch (error) { 831 | console.error('[工具] 复制邮箱失败:', error); 832 | updateStatus('复制失败: ' + error.message, 'error'); 833 | addLog('复制邮箱失败: ' + error.message, 'error'); 834 | } 835 | } 836 | 837 | /** 838 | * 生成新邮箱密码并更新到输入框 839 | */ 840 | function generateNewEmailPassword() { 841 | try { 842 | const newPassword = generateRandomPassword(); 843 | 844 | if (elements && elements.emailPassword) { 845 | elements.emailPassword.value = newPassword; 846 | // 确保密码类型正确 847 | if (elements.emailPassword.type === 'text') { 848 | elements.emailPassword.type = 'password'; 849 | } 850 | } 851 | 852 | updateStatus('邮箱密码已生成', 'success'); 853 | addLog('新邮箱密码已生成', 'success'); 854 | 855 | // 2秒后清除状态 856 | setTimeout(() => { 857 | updateStatus('等待开始...', 'info'); 858 | }, 2000); 859 | } catch (error) { 860 | console.error('[工具] 生成邮箱密码失败:', error); 861 | updateStatus('生成邮箱密码失败', 'error'); 862 | } 863 | } 864 | 865 | /** 866 | * 测试 Moemail API Key 867 | */ 868 | async function testMoemailApiKey() { 869 | try { 870 | const apiKeyInput = document.getElementById('moemailApiKey'); 871 | const apiKey = apiKeyInput?.value?.trim() || ''; 872 | 873 | if (!apiKey) { 874 | updateStatus('请先输入 API Key', 'error'); 875 | addLog('API Key 为空,无法测试', 'error'); 876 | return; 877 | } 878 | 879 | updateStatus('正在测试 API Key...', 'running'); 880 | addLog('正在测试 API Key...', 'info'); 881 | 882 | // 尝试创建一个测试邮箱来验证 API Key 883 | const response = await sendBackgroundMessage('moemailCreate', { name: 'test' }); 884 | 885 | if (response?.success) { 886 | updateStatus('API Key 测试成功', 'success'); 887 | addLog('API Key 验证通过', 'success'); 888 | 889 | // 删除测试邮箱 890 | if (response.data?.id) { 891 | try { 892 | await sendBackgroundMessage('moemailDelete', { emailId: response.data.id }); 893 | } catch (e) { 894 | console.warn('[工具] 删除测试邮箱失败:', e); 895 | } 896 | } 897 | } else { 898 | updateStatus('API Key 测试失败', 'error'); 899 | addLog('API Key 验证失败: ' + (response?.error || '未知错误'), 'error'); 900 | } 901 | 902 | // 3秒后清除状态 903 | setTimeout(() => { 904 | updateStatus('等待开始...', 'info'); 905 | }, 3000); 906 | } catch (error) { 907 | console.error('[工具] 测试 API Key 失败:', error); 908 | updateStatus('测试失败: ' + error.message, 'error'); 909 | addLog('测试 API Key 失败: ' + error.message, 'error'); 910 | 911 | // 3秒后清除状态 912 | setTimeout(() => { 913 | updateStatus('等待开始...', 'info'); 914 | }, 3000); 915 | } 916 | } 917 | 918 | /** 919 | * 清空日志 920 | */ 921 | function clearLog() { 922 | try { 923 | if (elements && elements.log) { 924 | elements.log.innerHTML = ''; 925 | addLog('日志已清空', 'info'); 926 | } 927 | } catch (error) { 928 | console.error('[工具] 清空日志失败:', error); 929 | } 930 | } 931 | 932 | /** 933 | * 切换使用说明显示/隐藏 934 | */ 935 | function toggleHelp() { 936 | const helpContent = document.getElementById('helpContent'); 937 | if (helpContent) { 938 | if (helpContent.style.display === 'none' || helpContent.style.display === '') { 939 | helpContent.style.display = 'block'; 940 | } else { 941 | helpContent.style.display = 'none'; 942 | } 943 | } 944 | } 945 | 946 | /** 947 | * 自动填卡功能 948 | */ 949 | async function autoFillCard() { 950 | try { 951 | addLog('正在启动自动填卡...', 'info'); 952 | 953 | // 获取当前标签页 954 | const queryOptions = { active: true, currentWindow: true }; 955 | const [currentTab] = await chrome.tabs.query(queryOptions); 956 | 957 | let targetTab = null; 958 | 959 | if (currentTab && currentTab.url && 960 | (currentTab.url.includes('windsurf.com/billing') || 961 | currentTab.url.includes('codeium.com/billing'))) { 962 | // 当前页面已经是 billing 页面 963 | addLog('当前页面已是账单页面', 'success'); 964 | updateStatus('当前页面已是账单页面', 'success'); 965 | targetTab = currentTab; 966 | 967 | // 等待页面加载完成 968 | await waitForPageLoad(targetTab.id); 969 | 970 | // 直接执行填卡 971 | addLog('正在执行填卡...', 'info'); 972 | await injectAndExecute(targetTab.id, 'autoFillCard'); 973 | return; 974 | } 975 | 976 | // 需要打开 billing 页面 977 | if (currentTab) { 978 | targetTab = currentTab; 979 | } else { 980 | // 创建新标签页 981 | const newTab = await chrome.tabs.create({ url: 'https://windsurf.com/billing/individual?plan=2', active: true }); 982 | targetTab = newTab; 983 | } 984 | 985 | // 打开 billing 页面 986 | const updateOptions = { 987 | url: 'https://windsurf.com/billing/individual?plan=2', 988 | active: true 989 | }; 990 | 991 | await chrome.tabs.update(targetTab.id, updateOptions); 992 | addLog('已打开账单页面', 'success'); 993 | 994 | // 等待页面加载完成 995 | await waitForPageLoad(targetTab.id); 996 | 997 | // 执行填卡 998 | // 先显示浮动面板 999 | try { 1000 | await injectAndExecute(targetTab.id, 'showFloatingPanel'); 1001 | } catch (e) { 1002 | console.warn('[工具] 无法显示浮动面板:', e); 1003 | } 1004 | 1005 | addLog('页面加载完成,正在执行填卡...', 'success'); 1006 | await injectAndExecute(targetTab.id, 'autoFillCard'); 1007 | 1008 | addLog('自动填卡完成', 'success'); 1009 | updateStatus('自动填卡完成', 'success'); 1010 | } catch (error) { 1011 | console.error('[工具] 自动填卡失败:', error); 1012 | addLog('自动填卡失败: ' + error.message, 'error'); 1013 | updateStatus('自动填卡失败: ' + error.message, 'error'); 1014 | } 1015 | } 1016 | 1017 | /** 1018 | * 初始化插件 1019 | * 初始化 elements 对象并调用必要的初始化函数 1020 | */ 1021 | async function initPopup() { 1022 | try { 1023 | // 初始化 DEFAULT_CONFIG 1024 | if (!DEFAULT_CONFIG) { 1025 | DEFAULT_CONFIG = getDefaultConfig(); 1026 | } 1027 | 1028 | // 初始化 DOM 元素引用 1029 | elements = { 1030 | 'emailPrefix': document.getElementById('emailPrefix'), 1031 | 'password': document.getElementById('password'), 1032 | 'emailPassword': document.getElementById('emailPassword'), 1033 | 'emailPreview': document.getElementById('emailPreview'), 1034 | 'togglePassword': document.getElementById('togglePassword'), 1035 | 'generatePassword': document.getElementById('generatePassword'), 1036 | 'generatePrefix': document.getElementById('generatePrefix'), 1037 | 'generateEmailPassword': document.getElementById('generateEmailPassword'), 1038 | 'startBtn': document.getElementById('startBtn'), 1039 | 'autoFillCardBtn': document.getElementById('autoFillCardBtn'), 1040 | 'openPageBtn': document.getElementById('openPageBtn'), 1041 | 'saveConfigBtn': document.getElementById('saveConfigBtn'), 1042 | 'status': document.getElementById('status'), 1043 | 'log': document.getElementById('log'), 1044 | 'exportAccountsBtn': document.getElementById('exportAccountsBtn'), 1045 | 'saveConfigBtn': document.getElementById('saveConfigBtn'), 1046 | 'testApiKey': document.getElementById('testApiKey'), 1047 | 'clearLog': document.getElementById('clearLog') 1048 | }; 1049 | 1050 | // 加载配置 1051 | await loadConfig(); 1052 | 1053 | // 绑定事件监听器 1054 | bindEventListeners(); 1055 | 1056 | // 检查并打开注册页面(如果需要) 1057 | await checkAndOpenRegistrationPage(); 1058 | 1059 | // 加载并显示公告 1060 | if (typeof loadAndDisplayNotice === 'function') { 1061 | await loadAndDisplayNotice(); 1062 | } 1063 | 1064 | console.log('[工具] 插件初始化完成'); 1065 | } catch (error) { 1066 | console.error('[工具] 插件初始化失败:', error); 1067 | } 1068 | } 1069 | 1070 | // DOM 加载完成后自动初始化 1071 | if (document.readyState === 'loading') { 1072 | document.addEventListener('DOMContentLoaded', initPopup); 1073 | } else { 1074 | // DOM 已经加载完成,直接初始化 1075 | initPopup(); 1076 | } 1077 | 1078 | -------------------------------------------------------------------------------- /content-script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Content Script 3 | * 处理来自 popup 的消息,执行注册和填卡操作 4 | */ 5 | 6 | console.log('[Content Script] Content script 已加载'); 7 | 8 | // 浮动面板相关 9 | let floatingPanel = null; 10 | let floatingButton = null; 11 | let isPanelVisible = false; 12 | 13 | // 防止重复操作的标志 14 | let continueButtonClicked = false; 15 | let passwordFilled = false; 16 | let passwordSubmitted = false; 17 | let turnstileHandled = false; // Turnstile 验证是否已处理 18 | let isCheckingTurnstile = false; // 是否正在检测人机验证 19 | let turnstileError = false; // 是否已经检测到 Turnstile 错误(An error occurred) 20 | 21 | const TURNSTILE_CONTINUE_CLICK_DELAY = 1500; // ms 22 | 23 | function startDelayedContinueRetry() { 24 | if (turnstileError) { 25 | console.log('[Content Script] 已检测到 Turnstile 错误,停止重试 Continue'); 26 | return; 27 | } 28 | const retryInterval = setInterval(() => { 29 | const retryButton = Array.from(document.querySelectorAll('button')).find(btn => { 30 | const btnText = (btn.textContent || btn.innerText || '').trim(); 31 | return (btnText === 'Continue' || btnText === '继续') && 32 | !btn.disabled && 33 | !btn.className.includes('text-sk-black/40') && 34 | !btnText.includes('Other') && 35 | !btnText.includes('options'); 36 | }); 37 | 38 | if (retryButton && !retryButton.disabled) { 39 | clearInterval(retryInterval); 40 | setTimeout(() => { 41 | retryButton.focus(); 42 | retryButton.click(); 43 | retryButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); 44 | retryButton.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); 45 | retryButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); 46 | 47 | console.log('[Content Script] 重试:已点击 Continue 按钮'); 48 | updateFloatingStatus('已点击 Continue,等待页面跳转...', 'info'); 49 | addFloatingLog('已点击 Continue 按钮(重试)', 'success'); 50 | 51 | setTimeout(() => { 52 | startListeningEmail(); 53 | }, 2000); 54 | }, TURNSTILE_CONTINUE_CLICK_DELAY); 55 | } 56 | }, 500); 57 | 58 | // 最多重试30秒 59 | setTimeout(() => { 60 | clearInterval(retryInterval); 61 | console.log('[Content Script] 重试超时,直接开始监听邮件'); 62 | startListeningEmail(); 63 | }, 30000); 64 | } 65 | 66 | const MOEMAIL_SESSION_KEYS = { 67 | id: 'windsurf_moemail_id', 68 | email: 'windsurf_moemail_email' 69 | }; 70 | 71 | const moemailMonitorState = { 72 | active: false, 73 | pollTimer: null, 74 | lastCode: '', 75 | cursor: '', 76 | retries: 0, 77 | maxRetries: 120, 78 | startedAt: null, 79 | pendingCode: '', 80 | processingCode: false 81 | }; 82 | 83 | function sendExtensionMessage(type, payload = {}) { 84 | return new Promise((resolve, reject) => { 85 | try { 86 | chrome.runtime.sendMessage({ type, payload }, (response) => { 87 | if (chrome.runtime.lastError) { 88 | reject(new Error(chrome.runtime.lastError.message)); 89 | return; 90 | } 91 | 92 | resolve(response); 93 | }); 94 | } catch (error) { 95 | reject(error); 96 | } 97 | }); 98 | } 99 | 100 | function storeMoemailSession({ id, email }) { 101 | if (!id || !email) { 102 | return; 103 | } 104 | 105 | sessionStorage.setItem(MOEMAIL_SESSION_KEYS.id, id); 106 | sessionStorage.setItem(MOEMAIL_SESSION_KEYS.email, email); 107 | } 108 | 109 | function clearMoemailSession() { 110 | sessionStorage.removeItem(MOEMAIL_SESSION_KEYS.id); 111 | sessionStorage.removeItem(MOEMAIL_SESSION_KEYS.email); 112 | } 113 | 114 | function getMoemailSession() { 115 | const id = sessionStorage.getItem(MOEMAIL_SESSION_KEYS.id); 116 | const email = sessionStorage.getItem(MOEMAIL_SESSION_KEYS.email); 117 | 118 | if (id && email) { 119 | return { id, email }; 120 | } 121 | 122 | return { id: null, email: null }; 123 | } 124 | 125 | /** 126 | * 检测是否存在人机验证(提交密码后调用) 127 | * 如果存在,监听验证完成;如果不存在,直接监听邮件消息 128 | */ 129 | function checkTurnstileAndListen() { 130 | if (isCheckingTurnstile || turnstileHandled) { 131 | console.log('[Content Script] 已在检测人机验证或已处理,跳过重复检测'); 132 | return; 133 | } 134 | 135 | isCheckingTurnstile = true; 136 | console.log('[Content Script] 开始检测人机验证...'); 137 | updateFloatingStatus('检测人机验证...', 'info'); 138 | addFloatingLog('开始检测人机验证', 'info'); 139 | 140 | let checkCount = 0; 141 | const maxChecks = 30; // 最多检查30次(15秒) 142 | 143 | const checkInterval = setInterval(() => { 144 | checkCount++; 145 | 146 | // 检查是否有人机验证页面 147 | const turnstileResponse = document.querySelector('input[name="cf-turnstile-response"], input[id*="cf-turnstile"], input[id*="cf-chl-widget"]'); 148 | 149 | // 查找所有可能的 Turnstile iframe 150 | const turnstileIframe = document.querySelector('iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], iframe[id*="cf-chl-widget"], iframe[id*="cf-chl"], iframe[src*="cloudflare"]'); 151 | 152 | // 也查找所有 iframe,检查是否包含 Turnstile 相关内容 153 | let allIframes = document.querySelectorAll('iframe'); 154 | let foundTurnstileIframe = turnstileIframe; 155 | 156 | if (!foundTurnstileIframe && allIframes.length > 0) { 157 | for (const iframe of allIframes) { 158 | const src = (iframe.src || '').toLowerCase(); 159 | const id = (iframe.id || '').toLowerCase(); 160 | if (src.includes('cloudflare') || src.includes('turnstile') || src.includes('challenge') || 161 | id.includes('cf-chl') || id.includes('turnstile')) { 162 | foundTurnstileIframe = iframe; 163 | break; 164 | } 165 | } 166 | } 167 | 168 | const errorBanner = Array.from(document.querySelectorAll('div,p,span')).find(el => { 169 | const text = (el.textContent || '').trim().toLowerCase(); 170 | return text.includes('an error occurred') || text.includes('please try again later'); 171 | }); 172 | 173 | const hasTurnstile = turnstileResponse !== null || 174 | foundTurnstileIframe !== null || 175 | document.body.textContent.includes('Please verify that you are human') || 176 | document.body.textContent.includes('verify that you are human'); 177 | 178 | if (errorBanner) { 179 | clearInterval(checkInterval); 180 | isCheckingTurnstile = false; 181 | turnstileError = true; 182 | console.warn('[Content Script] 人机验证出现错误提示,可能需要更换 IP'); 183 | const message = '检测到 Cloudflare 错误(请稍后再试或更换 IP)'; 184 | updateFloatingStatus(message, 'error'); 185 | addFloatingLog('人机验证失败:An error occurred. 请更换 IP 或稍后重试', 'error'); 186 | return; 187 | } 188 | 189 | if (hasTurnstile) { 190 | clearInterval(checkInterval); 191 | isCheckingTurnstile = false; 192 | console.log(`[Content Script] 检测到人机验证(第 ${checkCount} 次检查)`); 193 | updateFloatingStatus('检测到人机验证,请手动完成验证', 'info'); 194 | addFloatingLog(`检测到人机验证(等待了 ${checkCount * 0.5} 秒),请手动完成验证`, 'info'); 195 | 196 | // 开始监听验证完成 197 | startListeningTurnstileComplete(); 198 | return; 199 | } 200 | 201 | // 如果检查次数超过限制,认为没有人机验证 202 | if (checkCount >= maxChecks) { 203 | clearInterval(checkInterval); 204 | isCheckingTurnstile = false; 205 | console.log('[Content Script] 未检测到人机验证,开始监听邮件消息'); 206 | updateFloatingStatus('未检测到人机验证,开始监听邮件...', 'info'); 207 | addFloatingLog('未检测到人机验证,开始监听邮件消息', 'info'); 208 | 209 | // 直接开始监听邮件消息 210 | startListeningEmail(); 211 | } else { 212 | // 继续等待,显示等待状态 213 | if (checkCount % 4 === 0) { // 每2秒更新一次状态 214 | updateFloatingStatus(`检测人机验证... (${checkCount * 0.5}秒)`, 'info'); 215 | } 216 | } 217 | }, 500); // 每500ms检查一次 218 | } 219 | 220 | /** 221 | * 监听人机验证是否完成 222 | */ 223 | function startListeningTurnstileComplete() { 224 | if (turnstileError) { 225 | console.log('[Content Script] Turnstile 已出错,取消监听完成状态'); 226 | return; 227 | } 228 | console.log('[Content Script] 开始监听人机验证完成...'); 229 | updateFloatingStatus('等待人机验证完成...', 'info'); 230 | addFloatingLog('开始监听人机验证完成,请手动完成验证', 'info'); 231 | 232 | let checkCount = 0; 233 | const maxChecks = 120; // 最多检查120次(60秒) 234 | 235 | const checkInterval = setInterval(() => { 236 | checkCount++; 237 | 238 | // 1. 优先检查验证成功标志(#success 元素显示) 239 | const successElement = document.getElementById('success'); 240 | const successText = document.getElementById('success-text'); 241 | let isVerificationSuccess = false; 242 | 243 | if (successElement) { 244 | // 检查内联样式 245 | const inlineStyle = successElement.getAttribute('style') || ''; 246 | const hasInlineDisplay = inlineStyle.includes('display:') && !inlineStyle.includes('display: none'); 247 | const hasInlineVisibility = inlineStyle.includes('visibility: visible'); 248 | 249 | // 检查计算样式 250 | const style = window.getComputedStyle(successElement); 251 | const computedDisplay = style.display !== 'none'; 252 | const computedVisibility = style.visibility === 'visible'; 253 | 254 | // 检查文本内容 255 | const hasSuccessText = successText && (successText.textContent || successText.innerText || '').trim().includes('成功'); 256 | 257 | // 如果内联样式显示为 grid/flex/block 且 visibility 为 visible,或者计算样式显示可见,或者有成功文本 258 | if ((hasInlineDisplay && hasInlineVisibility) || (computedDisplay && computedVisibility) || hasSuccessText) { 259 | isVerificationSuccess = true; 260 | console.log('[Content Script] 检测到验证成功标志 (#success)', { 261 | inlineStyle, 262 | computedDisplay, 263 | computedVisibility, 264 | hasSuccessText 265 | }); 266 | } 267 | } 268 | 269 | // 2. 检查 Continue 按钮是否已启用(说明验证完成) 270 | const continueButton = Array.from(document.querySelectorAll('button')).find(btn => { 271 | const btnText = (btn.textContent || btn.innerText || '').trim(); 272 | return (btnText === 'Continue' || btnText === '继续') && 273 | !btn.className.includes('text-sk-black/40') && 274 | !btnText.includes('Other') && 275 | !btnText.includes('options'); 276 | }); 277 | 278 | // 3. 检查 turnstile-response 是否有值(说明验证完成) 279 | const turnstileResponse = document.querySelector('input[name="cf-turnstile-response"], input[id*="cf-turnstile"], input[id*="cf-chl-widget"]'); 280 | const hasResponseValue = turnstileResponse && turnstileResponse.value && turnstileResponse.value.length > 0; 281 | 282 | // 如果检测到验证成功标志,或者 Continue 按钮已启用,或者 response 有值 283 | if (isVerificationSuccess || (continueButton && !continueButton.disabled) || hasResponseValue) { 284 | clearInterval(checkInterval); 285 | console.log('[Content Script] 人机验证已完成'); 286 | 287 | if (isVerificationSuccess) { 288 | updateFloatingStatus('人机验证成功!正在点击 Continue...', 'success'); 289 | addFloatingLog('检测到验证成功标志', 'success'); 290 | } else if (continueButton && !continueButton.disabled) { 291 | updateFloatingStatus('人机验证已完成,Continue 按钮已启用,正在点击...', 'success'); 292 | addFloatingLog('Continue 按钮已启用', 'success'); 293 | } else { 294 | updateFloatingStatus('人机验证已完成(检测到响应值),正在点击 Continue...', 'success'); 295 | addFloatingLog('检测到验证响应值', 'success'); 296 | } 297 | 298 | // 查找并点击 Continue 按钮 299 | if (continueButton) { 300 | setTimeout(() => { 301 | // 确保按钮可见且未禁用 302 | if (!continueButton.disabled) { 303 | continueButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); 304 | setTimeout(() => { 305 | if (continueButton.disabled) { 306 | console.warn('[Content Script] 延迟后 Continue 按钮重新禁用,等待重试'); 307 | updateFloatingStatus('Continue 按钮仍不可点击,等待中...', 'info'); 308 | startDelayedContinueRetry(); 309 | return; 310 | } 311 | 312 | continueButton.focus(); 313 | continueButton.click(); 314 | 315 | // 触发事件以确保点击生效 316 | continueButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); 317 | continueButton.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); 318 | continueButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); 319 | 320 | console.log('[Content Script] 已点击 Continue 按钮'); 321 | updateFloatingStatus('已点击 Continue,等待页面跳转...', 'info'); 322 | addFloatingLog('已点击 Continue 按钮', 'success'); 323 | 324 | // 等待页面跳转后,开始监听邮件消息 325 | setTimeout(() => { 326 | startListeningEmail(); 327 | }, 2000); 328 | }, TURNSTILE_CONTINUE_CLICK_DELAY); 329 | } else { 330 | console.log('[Content Script] Continue 按钮仍被禁用,等待中...'); 331 | updateFloatingStatus('Continue 按钮仍被禁用,等待中...', 'info'); 332 | startDelayedContinueRetry(); 333 | } 334 | }, 500); 335 | } else { 336 | // 如果找不到 Continue 按钮,直接开始监听邮件 337 | console.log('[Content Script] 未找到 Continue 按钮,直接开始监听邮件'); 338 | updateFloatingStatus('验证完成,开始监听邮件...', 'info'); 339 | setTimeout(() => { 340 | startListeningEmail(); 341 | }, 1000); 342 | } 343 | return; 344 | } 345 | 346 | // 如果检查次数超过限制 347 | if (checkCount >= maxChecks) { 348 | clearInterval(checkInterval); 349 | console.log('[Content Script] 等待验证超时'); 350 | updateFloatingStatus('等待验证超时,请检查验证状态', 'error'); 351 | addFloatingLog('等待验证超时(60秒)', 'error'); 352 | } else { 353 | // 每10秒更新一次状态 354 | if (checkCount % 20 === 0) { 355 | updateFloatingStatus(`等待人机验证完成... (${checkCount * 0.5}秒)`, 'info'); 356 | } 357 | } 358 | }, 500); // 每500ms检查一次 359 | } 360 | 361 | /** 362 | * 监听邮件消息(验证完成后或没有验证时调用) 363 | */ 364 | function startListeningEmail() { 365 | if (turnstileError) { 366 | console.log('[Content Script] Turnstile 出错,跳过邮箱监听'); 367 | return; 368 | } 369 | console.log('[Content Script] 开始监听邮件消息...'); 370 | updateFloatingStatus('开始监听邮件消息...', 'info'); 371 | addFloatingLog('开始监听邮件消息,等待验证码输入框', 'info'); 372 | 373 | let verificationInputDetected = false; 374 | 375 | const checkVerificationCode = () => { 376 | const codeInputs = findVerificationInputs(); 377 | 378 | if (codeInputs.length > 0) { 379 | if (!verificationInputDetected) { 380 | console.log('[Content Script] 检测到验证码输入框'); 381 | updateFloatingStatus('检测到验证码输入框,准备获取验证码...', 'info'); 382 | addFloatingLog('检测到验证码输入框,准备获取验证码', 'info'); 383 | verificationInputDetected = true; 384 | } 385 | 386 | startMoemailVerificationFlow({ forceStart: true, attemptImmediateFill: true }); 387 | } 388 | }; 389 | 390 | // 立即检查一次 391 | checkVerificationCode(); 392 | 393 | // 使用 MutationObserver 监听 DOM 变化 394 | const observer = new MutationObserver(() => { 395 | checkVerificationCode(); 396 | }); 397 | 398 | observer.observe(document.body, { 399 | childList: true, 400 | subtree: true 401 | }); 402 | 403 | // 也监听 URL 变化 404 | let lastUrl = location.href; 405 | const urlCheckInterval = setInterval(() => { 406 | if (location.href !== lastUrl) { 407 | lastUrl = location.href; 408 | console.log('[Content Script] 页面 URL 已变化:', location.href); 409 | checkVerificationCode(); 410 | } 411 | }, 1000); 412 | 413 | // 3分钟后停止 DOM 观察(Moemail 监听会继续运行) 414 | setTimeout(() => { 415 | observer.disconnect(); 416 | clearInterval(urlCheckInterval); 417 | console.log('[Content Script] 停止 DOM 监听'); 418 | }, 180000); 419 | } 420 | 421 | function findVerificationInputs() { 422 | const selectors = [ 423 | 'input[type="text"][name*="code" i]', 424 | 'input[type="text"][id*="code" i]', 425 | 'input[type="text"][placeholder*="code" i]', 426 | 'input[type="number"][name*="code" i]', 427 | 'input[type="number"][id*="code" i]', 428 | 'input[type="tel"][name*="code" i]', 429 | 'input[type="text"][name*="otp" i]', 430 | 'input[type="text"][id*="otp" i]', 431 | 'input[placeholder*="otp" i]', 432 | 'input[autocomplete="one-time-code"]', 433 | 'input[name*="verification" i]', 434 | 'input[id*="verification" i]', 435 | 'input[placeholder*="verification" i]', 436 | 'input[name*="pin" i]', 437 | 'input[id*="pin" i]', 438 | 'input[placeholder*="pin" i]', 439 | 'input[data-otp-input]', 440 | 'input[placeholder*="验证码"]', 441 | 'input[aria-label*="code" i]', 442 | 'input[data-testid*="code" i]', 443 | 'input[data-testid*="verification" i]' 444 | ]; 445 | 446 | const results = []; 447 | const seen = new Set(); 448 | 449 | for (const selector of selectors) { 450 | const inputs = document.querySelectorAll(selector); 451 | for (const input of inputs) { 452 | if (!seen.has(input)) { 453 | results.push(input); 454 | seen.add(input); 455 | } 456 | } 457 | } 458 | 459 | if (results.length === 0) { 460 | const singleCharInputs = Array.from(document.querySelectorAll('input[maxlength="1"]')) 461 | .filter(input => { 462 | const type = (input.getAttribute('type') || '').toLowerCase(); 463 | return ['text', 'tel', ''].includes(type); 464 | }); 465 | 466 | if (singleCharInputs.length >= 4) { 467 | const groups = new Map(); 468 | 469 | for (const input of singleCharInputs) { 470 | const groupKey = input.closest('.flex') || input.parentElement || document.body; 471 | if (!groups.has(groupKey)) { 472 | groups.set(groupKey, []); 473 | } 474 | groups.get(groupKey).push(input); 475 | } 476 | 477 | const bestGroup = Array.from(groups.values()).sort((a, b) => b.length - a.length)[0]; 478 | 479 | if (bestGroup && bestGroup.length >= 4) { 480 | for (const input of bestGroup) { 481 | if (!seen.has(input)) { 482 | results.push(input); 483 | seen.add(input); 484 | } 485 | } 486 | } 487 | } 488 | } 489 | 490 | return results; 491 | } 492 | 493 | function triggerInputEvents(target) { 494 | if (!target) return; 495 | 496 | target.dispatchEvent(new Event('input', { bubbles: true })); 497 | target.dispatchEvent(new Event('change', { bubbles: true })); 498 | target.dispatchEvent(new Event('keyup', { bubbles: true })); 499 | } 500 | 501 | function fillVerificationCode(code) { 502 | if (!code) { 503 | return false; 504 | } 505 | 506 | const codeInputs = findVerificationInputs(); 507 | if (codeInputs.length === 0) { 508 | return false; 509 | } 510 | 511 | if (codeInputs.length === 1) { 512 | const input = codeInputs[0]; 513 | input.focus(); 514 | input.value = code; 515 | triggerInputEvents(input); 516 | return true; 517 | } 518 | 519 | const digits = code.split(''); 520 | for (let i = 0; i < codeInputs.length; i++) { 521 | const char = digits[i] || ''; 522 | const input = codeInputs[i]; 523 | input.focus(); 524 | input.value = char; 525 | triggerInputEvents(input); 526 | } 527 | 528 | return true; 529 | } 530 | 531 | async function submitVerificationCode() { 532 | const buttons = Array.from(document.querySelectorAll('button, input[type="submit"]')); 533 | const targetButton = buttons.find((btn) => { 534 | const text = (btn.textContent || btn.innerText || btn.value || '').trim().toLowerCase(); 535 | return text.includes('verify') || text.includes('验证') || text.includes('继续') || text.includes('submit') || text.includes('确认'); 536 | }); 537 | 538 | if (targetButton && !targetButton.disabled) { 539 | targetButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); 540 | setTimeout(() => { 541 | targetButton.click(); 542 | targetButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); 543 | }, 300); 544 | return true; 545 | } 546 | 547 | const codeInputs = findVerificationInputs(); 548 | const form = codeInputs[0]?.closest('form'); 549 | if (form) { 550 | form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); 551 | if (typeof form.submit === 'function') { 552 | form.submit(); 553 | } 554 | return true; 555 | } 556 | 557 | return false; 558 | } 559 | 560 | function clearMoemailPollingTimer() { 561 | if (moemailMonitorState.pollTimer) { 562 | clearInterval(moemailMonitorState.pollTimer); 563 | moemailMonitorState.pollTimer = null; 564 | } 565 | } 566 | 567 | function stopMoemailMonitor(message = '', type = 'info') { 568 | clearMoemailPollingTimer(); 569 | moemailMonitorState.active = false; 570 | moemailMonitorState.cursor = ''; 571 | moemailMonitorState.lastCode = ''; 572 | moemailMonitorState.retries = 0; 573 | moemailMonitorState.startedAt = null; 574 | moemailMonitorState.pendingCode = ''; 575 | moemailMonitorState.processingCode = false; 576 | 577 | if (message) { 578 | addFloatingLog(message, type); 579 | updateFloatingStatus(message, type === 'error' ? 'error' : 'success'); 580 | } 581 | } 582 | 583 | async function processPendingVerificationCode() { 584 | if (!moemailMonitorState.pendingCode || moemailMonitorState.processingCode) { 585 | return false; 586 | } 587 | 588 | moemailMonitorState.processingCode = true; 589 | 590 | try { 591 | const code = moemailMonitorState.pendingCode; 592 | const filled = fillVerificationCode(code); 593 | 594 | if (!filled) { 595 | addFloatingLog('收到验证码,但无法填写输入框', 'error'); 596 | return false; 597 | } 598 | 599 | updateFloatingStatus('验证码已填入,正在提交...', 'success'); 600 | const submitted = await submitVerificationCode(); 601 | 602 | if (submitted) { 603 | addFloatingLog('验证码已自动提交', 'success'); 604 | } else { 605 | addFloatingLog('验证码已填写,请手动提交', 'info'); 606 | } 607 | 608 | try { 609 | const { id: currentId } = getMoemailSession(); 610 | if (currentId) { 611 | await sendExtensionMessage('moemailDelete', { emailId: currentId }); 612 | clearMoemailSession(); 613 | addFloatingLog('Moemail 邮箱已删除', 'success'); 614 | } 615 | } catch (deleteError) { 616 | console.warn('[Content Script] 删除 Moemail 邮箱失败:', deleteError); 617 | addFloatingLog('删除 Moemail 邮箱失败: ' + deleteError.message, 'error'); 618 | } 619 | 620 | moemailMonitorState.pendingCode = ''; 621 | stopMoemailMonitor('验证码已填写并提交', 'success'); 622 | return true; 623 | } finally { 624 | moemailMonitorState.processingCode = false; 625 | } 626 | } 627 | 628 | function startMoemailVerificationFlow(options = {}) { 629 | const { forceStart = false, attemptImmediateFill = false } = options; 630 | const { id, email } = getMoemailSession(); 631 | 632 | if (!id || !email) { 633 | if (forceStart || attemptImmediateFill) { 634 | addFloatingLog('未找到 Moemail 邮箱信息,无法监听验证码', 'error'); 635 | } 636 | return; 637 | } 638 | 639 | if (attemptImmediateFill && moemailMonitorState.pendingCode) { 640 | processPendingVerificationCode().catch((err) => { 641 | console.error('[Content Script] 处理验证码失败:', err); 642 | }); 643 | } 644 | 645 | if (moemailMonitorState.active) { 646 | return; 647 | } 648 | 649 | moemailMonitorState.active = true; 650 | moemailMonitorState.cursor = ''; 651 | moemailMonitorState.lastCode = ''; 652 | moemailMonitorState.retries = 0; 653 | moemailMonitorState.startedAt = Date.now(); 654 | moemailMonitorState.pendingCode = ''; 655 | 656 | addFloatingLog(`开始监听邮箱验证码:${email}`, 'info'); 657 | updateFloatingStatus('正在监听邮箱验证码...', 'info'); 658 | 659 | const pollInbox = async () => { 660 | const { id: currentId } = getMoemailSession(); 661 | if (!currentId) { 662 | stopMoemailMonitor('Moemail 邮箱信息已失效', 'error'); 663 | return; 664 | } 665 | 666 | moemailMonitorState.retries += 1; 667 | if (moemailMonitorState.retries > moemailMonitorState.maxRetries) { 668 | stopMoemailMonitor('监听验证码超时,请手动检查邮箱', 'error'); 669 | return; 670 | } 671 | 672 | try { 673 | const response = await sendExtensionMessage('moemailFetchMessages', { 674 | emailId: currentId, 675 | cursor: moemailMonitorState.cursor 676 | }); 677 | 678 | if (!response?.success) { 679 | throw new Error(response?.error || '获取邮件失败'); 680 | } 681 | 682 | const data = response.data || {}; 683 | if (data.cursor) { 684 | moemailMonitorState.cursor = data.cursor; 685 | } 686 | 687 | const latestCode = data.latestCode; 688 | if (latestCode && latestCode !== moemailMonitorState.lastCode) { 689 | moemailMonitorState.lastCode = latestCode; 690 | moemailMonitorState.pendingCode = latestCode; 691 | addFloatingLog(`收到验证码:${latestCode}`, 'success'); 692 | clearMoemailPollingTimer(); 693 | processPendingVerificationCode().catch((err) => { 694 | console.error('[Content Script] 处理验证码失败:', err); 695 | }); 696 | } 697 | } catch (error) { 698 | console.error('[Content Script] 监听邮箱失败:', error); 699 | addFloatingLog('监听邮箱失败: ' + error.message, 'error'); 700 | } 701 | }; 702 | 703 | // 立即执行一次,然后每5秒轮询 704 | pollInbox(); 705 | moemailMonitorState.pollTimer = setInterval(pollInbox, 5000); 706 | } 707 | 708 | 709 | /** 710 | * 检查并填写密码(在下一个页面) 711 | */ 712 | function checkAndFillPassword() { 713 | const password = sessionStorage.getItem('windsurf_registration_password'); 714 | if (!password) { 715 | return; // 没有保存的密码,直接返回 716 | } 717 | 718 | // 如果已经填写并提交了密码,不再执行任何操作 719 | if (passwordSubmitted) { 720 | return; 721 | } 722 | 723 | // 1. 查找所有密码输入框(包括主密码和确认密码) 724 | const passwordSelectors = [ 725 | 'input[type="password"]', 726 | 'input[name*="password" i]', 727 | 'input[id*="password" i]', 728 | 'input[placeholder*="password" i]', 729 | 'input[autocomplete*="password" i]' 730 | ]; 731 | 732 | // 查找主密码输入框(排除确认密码) 733 | const mainPasswordSelectors = [ 734 | 'input[type="password"]:not([id*="confirm" i]):not([name*="confirm" i]):not([id*="Confirmation" i]):not([name*="Confirmation" i])', 735 | 'input[name*="password" i]:not([name*="confirm" i]):not([name*="Confirmation" i])', 736 | 'input[id*="password" i]:not([id*="confirm" i]):not([id*="Confirmation" i])', 737 | 'input[autocomplete="new-password"]:not([id*="confirm" i]):not([name*="confirm" i])', 738 | 'input[autocomplete="current-password"]' 739 | ]; 740 | 741 | // 查找确认密码输入框 742 | const confirmPasswordSelectors = [ 743 | 'input[id*="passwordConfirmation" i]', 744 | 'input[id*="confirmPassword" i]', 745 | 'input[name*="confirmPassword" i]', 746 | 'input[name*="passwordConfirmation" i]', 747 | 'input[placeholder*="confirm" i][type="password"]', 748 | 'input[placeholder*="Confirm" i][type="password"]' 749 | ]; 750 | 751 | let passwordInput = null; 752 | let confirmPasswordInput = null; 753 | 754 | // 先尝试通过特定选择器查找主密码 755 | for (const selector of mainPasswordSelectors) { 756 | try { 757 | const inputs = document.querySelectorAll(selector); 758 | for (const input of inputs) { 759 | // 排除确认密码字段 760 | const id = (input.id || '').toLowerCase(); 761 | const name = (input.name || '').toLowerCase(); 762 | if (!id.includes('confirm') && !name.includes('confirm') && 763 | !id.includes('confirmation') && !name.includes('confirmation')) { 764 | passwordInput = input; 765 | break; 766 | } 767 | } 768 | if (passwordInput) break; 769 | } catch (e) { 770 | // 选择器可能不支持,继续尝试 771 | } 772 | } 773 | 774 | // 如果没找到,使用通用选择器 775 | if (!passwordInput) { 776 | const allPasswordInputs = document.querySelectorAll('input[type="password"]'); 777 | for (const input of allPasswordInputs) { 778 | const id = (input.id || '').toLowerCase(); 779 | const name = (input.name || '').toLowerCase(); 780 | const placeholder = (input.placeholder || '').toLowerCase(); 781 | 782 | // 如果是确认密码字段,跳过 783 | if (id.includes('confirm') || name.includes('confirm') || 784 | id.includes('confirmation') || name.includes('confirmation') || 785 | placeholder.includes('confirm')) { 786 | continue; 787 | } 788 | 789 | passwordInput = input; 790 | break; 791 | } 792 | } 793 | 794 | // 查找确认密码输入框 795 | for (const selector of confirmPasswordSelectors) { 796 | confirmPasswordInput = document.querySelector(selector); 797 | if (confirmPasswordInput) break; 798 | } 799 | 800 | // 如果还是没找到确认密码,尝试查找所有密码输入框,选择第二个 801 | if (!confirmPasswordInput && passwordInput) { 802 | const allPasswordInputs = document.querySelectorAll('input[type="password"]'); 803 | if (allPasswordInputs.length > 1) { 804 | // 找到第二个密码输入框(通常是确认密码) 805 | for (let i = 0; i < allPasswordInputs.length; i++) { 806 | if (allPasswordInputs[i] !== passwordInput) { 807 | const id = (allPasswordInputs[i].id || '').toLowerCase(); 808 | const name = (allPasswordInputs[i].name || '').toLowerCase(); 809 | const placeholder = (allPasswordInputs[i].placeholder || '').toLowerCase(); 810 | 811 | // 如果包含 confirm 相关关键词,或者就是第二个密码输入框 812 | if (id.includes('confirm') || name.includes('confirm') || 813 | id.includes('confirmation') || name.includes('confirmation') || 814 | placeholder.includes('confirm') || 815 | placeholder.includes('Confirm')) { 816 | confirmPasswordInput = allPasswordInputs[i]; 817 | break; 818 | } 819 | } 820 | } 821 | 822 | // 如果还是没找到,就使用第二个密码输入框 823 | if (!confirmPasswordInput && allPasswordInputs.length > 1) { 824 | for (let i = 0; i < allPasswordInputs.length; i++) { 825 | if (allPasswordInputs[i] !== passwordInput) { 826 | confirmPasswordInput = allPasswordInputs[i]; 827 | break; 828 | } 829 | } 830 | } 831 | } 832 | } 833 | 834 | // 如果找到密码输入框 835 | if (passwordInput) { 836 | // 如果已经填写过密码,检查是否需要填写确认密码和提交 837 | if (passwordFilled) { 838 | // 检查密码是否还在(可能页面刷新了) 839 | if (passwordInput.value === password) { 840 | // 检查确认密码是否已填写 841 | if (confirmPasswordInput && !confirmPasswordInput.value) { 842 | confirmPasswordInput.focus(); 843 | confirmPasswordInput.value = password; 844 | confirmPasswordInput.dispatchEvent(new Event('input', { bubbles: true })); 845 | confirmPasswordInput.dispatchEvent(new Event('change', { bubbles: true })); 846 | console.log('[Content Script] 已填写确认密码'); 847 | 848 | // 等待一下再提交 849 | setTimeout(() => { 850 | submitPasswordForm(); 851 | }, 500); 852 | return; 853 | } 854 | 855 | // 密码和确认密码都已填写,尝试提交 856 | if (!passwordSubmitted) { 857 | setTimeout(() => { 858 | submitPasswordForm(); 859 | }, 500); 860 | } 861 | } 862 | return; 863 | } 864 | 865 | // 如果密码输入框存在但未填写,填写密码 866 | if (!passwordInput.value) { 867 | passwordInput.focus(); 868 | passwordInput.value = password; 869 | passwordInput.dispatchEvent(new Event('input', { bubbles: true })); 870 | passwordInput.dispatchEvent(new Event('change', { bubbles: true })); 871 | console.log('[Content Script] 已填写密码'); 872 | 873 | // 如果存在确认密码输入框,也填写 874 | if (confirmPasswordInput && !confirmPasswordInput.value) { 875 | // 等待一下再填写确认密码,让页面有时间处理第一个密码输入 876 | setTimeout(() => { 877 | confirmPasswordInput.focus(); 878 | confirmPasswordInput.value = password; 879 | confirmPasswordInput.dispatchEvent(new Event('input', { bubbles: true })); 880 | confirmPasswordInput.dispatchEvent(new Event('change', { bubbles: true })); 881 | console.log('[Content Script] 已填写确认密码'); 882 | }, 500); 883 | } 884 | 885 | passwordFilled = true; 886 | 887 | // 等待一段时间再提交,避免页面返回 888 | // 等待时间:如果有确认密码字段,等待更长时间(2秒),否则等待1.5秒 889 | const waitTime = confirmPasswordInput ? 2000 : 1500; 890 | console.log(`[Content Script] 填写完密码,等待 ${waitTime}ms 后提交,避免页面返回...`); 891 | updateFloatingStatus(`填写完密码,等待 ${waitTime/1000} 秒后提交...`, 'info'); 892 | addFloatingLog(`填写完密码,等待 ${waitTime/1000} 秒后提交,避免页面返回`, 'info'); 893 | 894 | setTimeout(() => { 895 | console.log('[Content Script] 等待时间结束,开始提交密码表单'); 896 | submitPasswordForm(); 897 | }, waitTime); 898 | return; 899 | } 900 | } 901 | 902 | // 提交密码表单的辅助函数 903 | function submitPasswordForm() { 904 | if (passwordSubmitted) { 905 | console.log('[Content Script] 密码已提交,跳过重复提交'); 906 | return; 907 | } 908 | 909 | console.log('[Content Script] 开始提交密码表单...'); 910 | 911 | // 保存当前 URL,用于检测页面是否跳转 912 | const currentUrl = location.href; 913 | const currentPath = location.pathname; 914 | 915 | // 防止页面返回(通过监听 beforeunload 和 popstate) 916 | const preventBack = (e) => { 917 | console.log('[Content Script] 检测到页面返回尝试,当前在密码页面,阻止返回'); 918 | // 不阻止,但记录 919 | }; 920 | 921 | window.addEventListener('beforeunload', preventBack, { once: true }); 922 | window.addEventListener('popstate', preventBack, { once: true }); 923 | 924 | // 检查是否有人机验证页面(Cloudflare Turnstile) 925 | const turnstileResponse = document.querySelector('input[name="cf-turnstile-response"], input[id*="cf-turnstile"], input[id*="cf-chl-widget"]'); 926 | const hasTurnstile = turnstileResponse !== null || 927 | document.body.textContent.includes('Please verify that you are human') || 928 | document.body.textContent.includes('verify that you are human'); 929 | 930 | if (hasTurnstile && !turnstileHandled) { 931 | console.log('[Content Script] 检测到 Cloudflare Turnstile 人机验证'); 932 | updateFloatingStatus('检测到人机验证,正在自动处理...', 'info'); 933 | addFloatingLog('检测到 Cloudflare Turnstile 验证', 'info'); 934 | 935 | turnstileHandled = true; 936 | 937 | // 开始监听验证完成(不再自动点击) 938 | startListeningTurnstileComplete(); 939 | return; // 等待验证完成后再继续 940 | } 941 | 942 | // 检查其他类型的人机验证 943 | const captchaSelectors = [ 944 | 'iframe[src*="recaptcha"]', 945 | 'iframe[src*="captcha"]', 946 | 'iframe[src*="turnstile"]', 947 | 'div[id*="recaptcha"]', 948 | 'div[class*="recaptcha"]', 949 | 'div[id*="captcha"]', 950 | 'div[class*="captcha"]', 951 | 'div[id*="turnstile"]', 952 | 'div[class*="turnstile"]', 953 | 'canvas[id*="captcha"]', 954 | 'img[src*="captcha"]' 955 | ]; 956 | 957 | let hasCaptcha = false; 958 | for (const selector of captchaSelectors) { 959 | const captcha = document.querySelector(selector); 960 | if (captcha) { 961 | hasCaptcha = true; 962 | console.log('[Content Script] 检测到人机验证,等待用户完成验证...'); 963 | updateFloatingStatus('检测到人机验证,请手动完成验证', 'info'); 964 | addFloatingLog('检测到人机验证,请手动完成验证后继续', 'info'); 965 | break; 966 | } 967 | } 968 | 969 | // 优先查找 "Continue" 按钮(密码页面应该点击这个) 970 | let submitButton = null; 971 | 972 | // 1. 首先查找包含 "Continue" 文本的按钮,但排除 "Other Sign up options" 973 | const allButtons = document.querySelectorAll('button'); 974 | for (const btn of allButtons) { 975 | const btnText = (btn.textContent || btn.innerText || '').trim(); 976 | const btnClass = (btn.className || '').toLowerCase(); 977 | 978 | // 检查是否是 "Continue" 按钮,并且不是 "Other Sign up options" 979 | if (btnText === 'Continue' || btnText === '继续') { 980 | // 排除包含 "Other" 或 "options" 的按钮 981 | if (!btnText.includes('Other') && !btnText.includes('options') && 982 | !btnClass.includes('text-sk-black/40')) { 983 | submitButton = btn; 984 | console.log('[Content Script] 找到 Continue 按钮:', btnText); 985 | break; 986 | } 987 | } 988 | } 989 | 990 | // 2. 如果没找到 Continue 按钮,尝试查找 type="submit" 的按钮 991 | if (!submitButton) { 992 | submitButton = document.querySelector('button[type="submit"], button[class*="submit" i], button[id*="submit" i], input[type="submit"]'); 993 | } 994 | 995 | // 3. 如果还是没找到,查找包含特定 class 的按钮(根据用户提供的 class) 996 | if (!submitButton) { 997 | // 查找包含 bg-sk-aqua 的按钮(Continue 按钮的特征) 998 | submitButton = document.querySelector('button.bg-sk-aqua, button[class*="bg-sk-aqua"]'); 999 | } 1000 | 1001 | if (submitButton && submitButton.disabled) { 1002 | console.log('[Content Script] 提交按钮被禁用,可能表单验证未通过或需要人机验证'); 1003 | updateFloatingStatus('提交按钮被禁用,请检查表单或完成验证', 'error'); 1004 | addFloatingLog('提交按钮被禁用,请检查表单或完成验证', 'error'); 1005 | 1006 | // 等待一下,可能验证会完成 1007 | setTimeout(() => { 1008 | if (!submitButton.disabled && !passwordSubmitted) { 1009 | console.log('[Content Script] 提交按钮已启用,重新尝试提交'); 1010 | submitPasswordForm(); 1011 | } 1012 | }, 3000); 1013 | return; 1014 | } 1015 | 1016 | if (submitButton && !passwordSubmitted) { 1017 | // 先检查表单验证 1018 | const form = submitButton.closest('form'); 1019 | 1020 | // 检查密码和确认密码是否匹配 1021 | if (passwordInput && confirmPasswordInput) { 1022 | if (passwordInput.value !== confirmPasswordInput.value) { 1023 | console.warn('[Content Script] 密码和确认密码不匹配,重新填写确认密码'); 1024 | confirmPasswordInput.value = passwordInput.value; 1025 | confirmPasswordInput.dispatchEvent(new Event('input', { bubbles: true })); 1026 | confirmPasswordInput.dispatchEvent(new Event('change', { bubbles: true })); 1027 | // 等待一下让页面处理 1028 | setTimeout(() => { 1029 | if (!passwordSubmitted) { 1030 | submitPasswordForm(); 1031 | } 1032 | }, 500); 1033 | return; 1034 | } 1035 | } 1036 | 1037 | if (form && !form.checkValidity()) { 1038 | console.log('[Content Script] 表单验证未通过,尝试触发验证'); 1039 | form.reportValidity(); 1040 | 1041 | // 等待验证完成,但时间缩短 1042 | setTimeout(() => { 1043 | if (form.checkValidity() && !passwordSubmitted) { 1044 | console.log('[Content Script] 表单验证已通过,重新提交'); 1045 | submitPasswordForm(); 1046 | } else { 1047 | console.warn('[Content Script] 表单验证仍然未通过,但继续尝试提交'); 1048 | // 即使验证未通过,也尝试提交(某些网站可能只是警告) 1049 | if (!passwordSubmitted) { 1050 | submitButton.click(); 1051 | passwordSubmitted = true; 1052 | } 1053 | } 1054 | }, 1000); 1055 | return; 1056 | } 1057 | 1058 | // 监听表单提交事件 1059 | if (form) { 1060 | let formSubmitted = false; 1061 | form.addEventListener('submit', (e) => { 1062 | formSubmitted = true; 1063 | console.log('[Content Script] 表单正在提交...'); 1064 | // 不阻止提交,让表单正常提交 1065 | }, { once: true }); 1066 | 1067 | // 如果表单没有触发 submit 事件,可能是通过按钮点击触发的 1068 | setTimeout(() => { 1069 | if (!formSubmitted) { 1070 | console.log('[Content Script] 表单可能通过按钮点击提交,等待页面响应...'); 1071 | } 1072 | }, 1000); 1073 | } 1074 | 1075 | // 最后再等待一下,确保所有验证都完成,避免页面返回 1076 | setTimeout(() => { 1077 | // 再次检查按钮是否可用 1078 | if (submitButton.disabled) { 1079 | console.log('[Content Script] 提交按钮仍然被禁用,等待更长时间...'); 1080 | updateFloatingStatus('提交按钮被禁用,等待验证完成...', 'info'); 1081 | setTimeout(() => { 1082 | if (!submitButton.disabled && !passwordSubmitted) { 1083 | submitButton.focus(); 1084 | submitButton.click(); 1085 | passwordSubmitted = true; 1086 | console.log('[Content Script] 已点击密码页面的提交按钮(延迟后)'); 1087 | updateFloatingStatus('已提交密码表单,等待页面跳转...', 'info'); 1088 | addFloatingLog('已点击提交按钮(延迟后)', 'success'); 1089 | } 1090 | }, 2000); 1091 | return; 1092 | } 1093 | 1094 | // 点击提交按钮 1095 | submitButton.focus(); 1096 | submitButton.click(); 1097 | passwordSubmitted = true; 1098 | console.log('[Content Script] 已点击密码页面的提交按钮'); 1099 | updateFloatingStatus('已提交密码表单,等待页面跳转...', 'info'); 1100 | addFloatingLog('已点击提交按钮', 'success'); 1101 | 1102 | // 等待5秒后检测人机验证框并自动点击 1103 | setTimeout(() => { 1104 | console.log('[Content Script] 开始检测人机验证框...'); 1105 | updateFloatingStatus('检测人机验证框...', 'info'); 1106 | addFloatingLog('开始检测人机验证框', 'info'); 1107 | 1108 | // 检测并处理 Turnstile 验证 1109 | checkTurnstileAndListen(); 1110 | }, 5000); // 等待5秒 1111 | }, 500); // 额外等待500ms,确保页面稳定 1112 | 1113 | // 监听页面变化 1114 | let checkCount = 0; 1115 | const maxChecks = 20; // 最多检查20次(10秒) 1116 | const checkInterval = setInterval(() => { 1117 | checkCount++; 1118 | const newUrl = location.href; 1119 | const newPath = location.pathname; 1120 | 1121 | // 如果 URL 或路径发生变化,说明页面已跳转 1122 | if (newUrl !== currentUrl || newPath !== currentPath) { 1123 | clearInterval(checkInterval); 1124 | console.log('[Content Script] 页面已跳转:', newUrl); 1125 | updateFloatingStatus('页面已跳转,等待下一步...', 'success'); 1126 | addFloatingLog('页面已跳转: ' + newPath, 'success'); 1127 | 1128 | // 重置标志,准备处理新页面 1129 | setTimeout(() => { 1130 | continueButtonClicked = false; 1131 | passwordFilled = false; 1132 | passwordSubmitted = false; 1133 | }, 2000); 1134 | return; 1135 | } 1136 | 1137 | // 检查是否回到了注册初始页面(说明提交失败或页面刷新) 1138 | if (location.pathname.includes('/register') && 1139 | !location.pathname.includes('password') && 1140 | !location.pathname.includes('confirm') && 1141 | document.querySelector('input[name*="first" i], input[id*="first" i]')) { 1142 | clearInterval(checkInterval); 1143 | console.warn('[Content Script] 检测到页面回到了注册初始页面,可能提交失败或需要重新填写'); 1144 | updateFloatingStatus('页面回到注册页面,可能提交失败', 'error'); 1145 | addFloatingLog('页面回到注册页面,可能提交失败或需要人机验证', 'error'); 1146 | 1147 | // 重置标志,允许重新填写 1148 | setTimeout(() => { 1149 | continueButtonClicked = false; 1150 | passwordFilled = false; 1151 | passwordSubmitted = false; 1152 | // 清除保存的密码,避免自动填写 1153 | sessionStorage.removeItem('windsurf_registration_password'); 1154 | }, 2000); 1155 | return; 1156 | } 1157 | 1158 | // 如果检查次数超过限制,停止检查 1159 | if (checkCount >= maxChecks) { 1160 | clearInterval(checkInterval); 1161 | console.log('[Content Script] 页面未跳转,可能仍在处理中或需要人机验证'); 1162 | 1163 | // 检查是否还在密码页面 1164 | if (location.pathname.includes('password') || 1165 | (location.pathname.includes('register') && document.querySelector('input[type="password"]'))) { 1166 | console.log('[Content Script] 仍在密码页面,可能需要人机验证'); 1167 | updateFloatingStatus('可能需要人机验证,请检查页面', 'info'); 1168 | addFloatingLog('页面未跳转,可能需要人机验证', 'info'); 1169 | } else if (location.pathname.includes('/register') && 1170 | !location.pathname.includes('password') && 1171 | document.querySelector('input[name*="first" i], input[id*="first" i]')) { 1172 | // 回到了注册初始页面 1173 | console.warn('[Content Script] 检测到页面回到了注册初始页面'); 1174 | updateFloatingStatus('页面回到注册页面,可能提交失败', 'error'); 1175 | addFloatingLog('页面回到注册页面,可能提交失败或需要人机验证', 'error'); 1176 | } else { 1177 | console.log('[Content Script] 页面可能已跳转但 URL 未变化(SPA)'); 1178 | updateFloatingStatus('等待页面加载...', 'info'); 1179 | } 1180 | } 1181 | }, 500); 1182 | 1183 | } else { 1184 | // 如果没找到 submit 按钮,尝试查找其他提交按钮 1185 | // 优先查找 "Continue" 按钮 1186 | if (!submitButton) { 1187 | const allButtons = document.querySelectorAll('button'); 1188 | for (const btn of allButtons) { 1189 | const btnText = (btn.textContent || btn.innerText || '').trim(); 1190 | const btnClass = (btn.className || '').toLowerCase(); 1191 | 1192 | // 优先查找 "Continue" 按钮 1193 | if ((btnText === 'Continue' || btnText === '继续') && 1194 | !btnText.includes('Other') && 1195 | !btnText.includes('options') && 1196 | !btnClass.includes('text-sk-black/40')) { 1197 | submitButton = btn; 1198 | console.log('[Content Script] 找到 Continue 按钮:', btnText); 1199 | break; 1200 | } 1201 | } 1202 | } 1203 | 1204 | // 如果还是没找到,查找其他提交按钮 1205 | if (!submitButton) { 1206 | const allButtons = document.querySelectorAll('button'); 1207 | for (const btn of allButtons) { 1208 | const btnText = (btn.textContent || btn.innerText || '').trim(); 1209 | const btnClass = (btn.className || '').toLowerCase(); 1210 | 1211 | // 排除 "Other Sign up options" 按钮(通过 class 或文本判断) 1212 | if (btnText.includes('Other') || btnText.includes('options') || 1213 | btnClass.includes('text-sk-black/40')) { 1214 | continue; 1215 | } 1216 | 1217 | if ((btnText.includes('Submit') || 1218 | btnText.includes('Create') || 1219 | btnText.includes('Sign up') || 1220 | btnText.includes('Register') || 1221 | btnText.includes('Continue')) && !passwordSubmitted) { 1222 | submitButton = btn; 1223 | console.log('[Content Script] 找到提交按钮:', btnText); 1224 | break; 1225 | } 1226 | } 1227 | } 1228 | 1229 | // 如果找到了按钮,点击它 1230 | if (submitButton && !passwordSubmitted) { 1231 | submitButton.focus(); 1232 | submitButton.click(); 1233 | passwordSubmitted = true; 1234 | const btnText = (submitButton.textContent || submitButton.innerText || '').trim(); 1235 | console.log('[Content Script] 已点击提交按钮:', btnText); 1236 | updateFloatingStatus('已提交密码表单,等待页面跳转...', 'info'); 1237 | addFloatingLog('已点击提交按钮: ' + btnText, 'success'); 1238 | } else if (!submitButton) { 1239 | console.warn('[Content Script] 未找到提交按钮'); 1240 | updateFloatingStatus('未找到提交按钮', 'error'); 1241 | addFloatingLog('未找到提交按钮', 'error'); 1242 | } 1243 | } 1244 | } 1245 | 1246 | // 2. 如果没有密码输入框,检查是否有 "Continue" 按钮需要点击(但只点击一次) 1247 | if (!passwordInput && !continueButtonClicked) { 1248 | // 查找包含 "Continue" 文本的按钮 1249 | const allButtons = document.querySelectorAll('button'); 1250 | for (const btn of allButtons) { 1251 | const btnText = (btn.textContent || btn.innerText || '').trim(); 1252 | if (btnText.includes('Continue') || btnText.includes('继续')) { 1253 | // 检查按钮是否可见且可点击 1254 | const rect = btn.getBoundingClientRect(); 1255 | const isVisible = rect.width > 0 && rect.height > 0 && 1256 | window.getComputedStyle(btn).display !== 'none' && 1257 | window.getComputedStyle(btn).visibility !== 'hidden'; 1258 | 1259 | if (isVisible && !continueButtonClicked) { 1260 | continueButtonClicked = true; 1261 | console.log('[Content Script] 检测到 Continue 按钮,正在点击...'); 1262 | btn.click(); 1263 | 1264 | // 点击后等待页面变化,然后再次检查密码输入框 1265 | setTimeout(() => { 1266 | checkAndFillPassword(); 1267 | }, 2000); 1268 | return; 1269 | } 1270 | } 1271 | } 1272 | } 1273 | } 1274 | 1275 | // 检查 Turnstile 验证的函数 1276 | function checkTurnstileVerification() { 1277 | const turnstileResponse = document.querySelector('input[name="cf-turnstile-response"], input[id*="cf-turnstile"], input[id*="cf-chl-widget"]'); 1278 | const hasTurnstile = turnstileResponse !== null || 1279 | document.body.textContent.includes('Please verify that you are human') || 1280 | document.body.textContent.includes('verify that you are human'); 1281 | 1282 | if (hasTurnstile && !turnstileHandled) { 1283 | console.log('[Content Script] 检测到 Cloudflare Turnstile 人机验证'); 1284 | updateFloatingStatus('检测到人机验证,请手动完成验证', 'info'); 1285 | addFloatingLog('检测到 Cloudflare Turnstile 验证,请手动完成', 'info'); 1286 | 1287 | turnstileHandled = true; 1288 | // 开始监听验证完成 1289 | startListeningTurnstileComplete(); 1290 | return true; // 返回 true 表示检测到验证,不需要继续检查密码 1291 | } 1292 | return false; // 返回 false 表示没有检测到验证,继续检查密码 1293 | } 1294 | 1295 | // 页面加载完成后,检查是否需要填写密码或处理验证 1296 | if (document.readyState === 'loading') { 1297 | document.addEventListener('DOMContentLoaded', () => { 1298 | setTimeout(() => { 1299 | if (!checkTurnstileVerification()) { 1300 | checkAndFillPassword(); 1301 | } 1302 | }, 1000); 1303 | }); 1304 | } else { 1305 | setTimeout(() => { 1306 | if (!checkTurnstileVerification()) { 1307 | checkAndFillPassword(); 1308 | } 1309 | }, 1000); 1310 | } 1311 | 1312 | // 监听页面变化(SPA 应用可能使用 pushState) 1313 | let lastUrl = location.href; 1314 | let checkTimeout = null; 1315 | 1316 | const urlChangeObserver = new MutationObserver(() => { 1317 | const url = location.href; 1318 | if (url !== lastUrl) { 1319 | lastUrl = url; 1320 | // 重置标志,因为页面变化了 1321 | continueButtonClicked = false; 1322 | passwordFilled = false; 1323 | passwordSubmitted = false; 1324 | 1325 | // 清除之前的定时器 1326 | if (checkTimeout) { 1327 | clearTimeout(checkTimeout); 1328 | } 1329 | 1330 | // 延迟检查,避免频繁触发 1331 | checkTimeout = setTimeout(() => { 1332 | checkAndFillPassword(); 1333 | }, 1000); 1334 | } 1335 | }); 1336 | 1337 | urlChangeObserver.observe(document, { subtree: true, childList: true }); 1338 | 1339 | // 监听 pushState 和 replaceState(SPA 路由变化) 1340 | const originalPushState = history.pushState; 1341 | const originalReplaceState = history.replaceState; 1342 | 1343 | history.pushState = function(...args) { 1344 | originalPushState.apply(history, args); 1345 | // 重置标志 1346 | continueButtonClicked = false; 1347 | passwordFilled = false; 1348 | passwordSubmitted = false; 1349 | 1350 | if (checkTimeout) { 1351 | clearTimeout(checkTimeout); 1352 | } 1353 | 1354 | checkTimeout = setTimeout(() => { 1355 | turnstileHandled = false; // 重置 Turnstile 标志 1356 | if (!checkTurnstileVerification()) { 1357 | checkAndFillPassword(); 1358 | } 1359 | }, 1000); 1360 | }; 1361 | 1362 | history.replaceState = function(...args) { 1363 | originalReplaceState.apply(history, args); 1364 | // 重置标志 1365 | continueButtonClicked = false; 1366 | passwordFilled = false; 1367 | passwordSubmitted = false; 1368 | turnstileHandled = false; 1369 | 1370 | if (checkTimeout) { 1371 | clearTimeout(checkTimeout); 1372 | } 1373 | 1374 | checkTimeout = setTimeout(() => { 1375 | if (!checkTurnstileVerification()) { 1376 | checkAndFillPassword(); 1377 | } 1378 | }, 1000); 1379 | }; 1380 | 1381 | /** 1382 | * 创建浮动面板 1383 | */ 1384 | function createFloatingPanel() { 1385 | if (floatingPanel) return; 1386 | 1387 | // 创建样式 1388 | const style = document.createElement('style'); 1389 | style.textContent = ` 1390 | #windsurf-floating-panel { 1391 | position: fixed; 1392 | top: 20px; 1393 | right: 20px; 1394 | width: 380px; 1395 | max-height: 85vh; 1396 | background: #FFFFFF; 1397 | border-radius: 12px; 1398 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 1399 | z-index: 999999; 1400 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 1401 | display: none; 1402 | overflow: hidden; 1403 | border: 2px solid #F45805; 1404 | } 1405 | #windsurf-floating-panel.visible { 1406 | display: block; 1407 | } 1408 | #windsurf-floating-panel-header { 1409 | background: linear-gradient(135deg, #F45805 0%, #0344A1 100%); 1410 | color: #FFFFFF; 1411 | padding: 12px 16px; 1412 | display: flex; 1413 | justify-content: space-between; 1414 | align-items: center; 1415 | cursor: move; 1416 | } 1417 | #windsurf-floating-panel-title { 1418 | font-size: 16px; 1419 | font-weight: 700; 1420 | } 1421 | #windsurf-floating-panel-close { 1422 | background: rgba(255, 255, 255, 0.2); 1423 | border: none; 1424 | color: #FFFFFF; 1425 | width: 24px; 1426 | height: 24px; 1427 | border-radius: 50%; 1428 | cursor: pointer; 1429 | font-size: 18px; 1430 | line-height: 1; 1431 | } 1432 | #windsurf-floating-panel-close:hover { 1433 | background: rgba(255, 255, 255, 0.3); 1434 | } 1435 | #windsurf-floating-panel-content { 1436 | padding: 16px; 1437 | max-height: calc(85vh - 60px); 1438 | overflow-y: auto; 1439 | } 1440 | #windsurf-floating-panel-content .status { 1441 | padding: 10px; 1442 | border-radius: 6px; 1443 | margin-bottom: 10px; 1444 | font-size: 13px; 1445 | text-align: center; 1446 | } 1447 | #windsurf-floating-panel-content .status.success { 1448 | background: #d4edda; 1449 | color: #155724; 1450 | } 1451 | #windsurf-floating-panel-content .status.error { 1452 | background: #f8d7da; 1453 | color: #721c24; 1454 | } 1455 | #windsurf-floating-panel-content .log { 1456 | max-height: 200px; 1457 | overflow-y: auto; 1458 | font-size: 12px; 1459 | line-height: 1.5; 1460 | padding: 8px; 1461 | background: #f8f9fa; 1462 | border-radius: 4px; 1463 | margin-top: 10px; 1464 | } 1465 | #windsurf-floating-button { 1466 | position: fixed; 1467 | bottom: 20px; 1468 | right: 20px; 1469 | width: 60px; 1470 | height: 60px; 1471 | background: linear-gradient(135deg, #F45805 0%, #0344A1 100%); 1472 | border-radius: 50%; 1473 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); 1474 | z-index: 999998; 1475 | cursor: pointer; 1476 | display: flex; 1477 | align-items: center; 1478 | justify-content: center; 1479 | font-size: 24px; 1480 | color: #FFFFFF; 1481 | border: 3px solid #FFFFFF; 1482 | transition: transform 0.2s; 1483 | } 1484 | #windsurf-floating-button:hover { 1485 | transform: scale(1.1); 1486 | } 1487 | #windsurf-floating-button.hidden { 1488 | display: none; 1489 | } 1490 | `; 1491 | document.head.appendChild(style); 1492 | 1493 | // 创建浮动按钮 1494 | floatingButton = document.createElement('div'); 1495 | floatingButton.id = 'windsurf-floating-button'; 1496 | floatingButton.textContent = '🚀'; 1497 | floatingButton.title = '打开控制面板'; 1498 | floatingButton.addEventListener('click', () => { 1499 | showFloatingPanel(); 1500 | }); 1501 | document.body.appendChild(floatingButton); 1502 | 1503 | // 创建面板 1504 | floatingPanel = document.createElement('div'); 1505 | floatingPanel.id = 'windsurf-floating-panel'; 1506 | 1507 | floatingPanel.innerHTML = ` 1508 |
1509 |
🚀 Windsurf 工具
1510 | 1511 |
1512 |
1513 |
等待开始...
1514 |
1515 |
1516 | `; 1517 | 1518 | document.body.appendChild(floatingPanel); 1519 | 1520 | // 关闭按钮 1521 | const closeBtn = floatingPanel.querySelector('#windsurf-floating-panel-close'); 1522 | closeBtn.addEventListener('click', () => { 1523 | hideFloatingPanel(); 1524 | }); 1525 | 1526 | // 拖拽功能 1527 | let isDragging = false; 1528 | let currentX, currentY, initialX, initialY; 1529 | 1530 | const header = floatingPanel.querySelector('#windsurf-floating-panel-header'); 1531 | header.addEventListener('mousedown', (e) => { 1532 | isDragging = true; 1533 | initialX = e.clientX - floatingPanel.offsetLeft; 1534 | initialY = e.clientY - floatingPanel.offsetTop; 1535 | }); 1536 | 1537 | document.addEventListener('mousemove', (e) => { 1538 | if (isDragging) { 1539 | e.preventDefault(); 1540 | currentX = e.clientX - initialX; 1541 | currentY = e.clientY - initialY; 1542 | floatingPanel.style.left = currentX + 'px'; 1543 | floatingPanel.style.top = currentY + 'px'; 1544 | floatingPanel.style.right = 'auto'; 1545 | } 1546 | }); 1547 | 1548 | document.addEventListener('mouseup', () => { 1549 | isDragging = false; 1550 | }); 1551 | } 1552 | 1553 | /** 1554 | * 显示浮动面板 1555 | */ 1556 | function showFloatingPanel() { 1557 | if (!floatingPanel) { 1558 | createFloatingPanel(); 1559 | } 1560 | floatingPanel.classList.add('visible'); 1561 | if (floatingButton) { 1562 | floatingButton.classList.add('hidden'); 1563 | } 1564 | isPanelVisible = true; 1565 | } 1566 | 1567 | /** 1568 | * 隐藏浮动面板 1569 | */ 1570 | function hideFloatingPanel() { 1571 | if (floatingPanel) { 1572 | floatingPanel.classList.remove('visible'); 1573 | } 1574 | if (floatingButton) { 1575 | floatingButton.classList.remove('hidden'); 1576 | } 1577 | isPanelVisible = false; 1578 | } 1579 | 1580 | /** 1581 | * 更新浮动面板状态 1582 | */ 1583 | function updateFloatingStatus(message, type = 'info') { 1584 | if (!floatingPanel) return; 1585 | const statusEl = floatingPanel.querySelector('#floating-status'); 1586 | if (statusEl) { 1587 | statusEl.textContent = message; 1588 | statusEl.className = 'status ' + (type === 'success' ? 'success' : type === 'error' ? 'error' : ''); 1589 | } 1590 | } 1591 | 1592 | /** 1593 | * 添加浮动面板日志 1594 | */ 1595 | function addFloatingLog(message, type = 'info') { 1596 | if (!floatingPanel) return; 1597 | const logEl = floatingPanel.querySelector('#floating-log'); 1598 | if (logEl) { 1599 | const time = new Date().toLocaleTimeString(); 1600 | const logItem = document.createElement('div'); 1601 | logItem.className = 'log-item ' + type; 1602 | logItem.textContent = `[${time}] ${message}`; 1603 | logEl.appendChild(logItem); 1604 | logEl.scrollTop = logEl.scrollHeight; 1605 | } 1606 | } 1607 | 1608 | // 初始化浮动按钮(页面加载时) 1609 | if (document.readyState === 'loading') { 1610 | document.addEventListener('DOMContentLoaded', () => { 1611 | setTimeout(() => { 1612 | createFloatingPanel(); 1613 | }, 500); 1614 | }); 1615 | } else { 1616 | setTimeout(() => { 1617 | createFloatingPanel(); 1618 | }, 500); 1619 | } 1620 | 1621 | // 监听来自 popup 或 background 的消息 1622 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 1623 | console.log('[Content Script] 收到消息:', message); 1624 | 1625 | // 异步处理消息 1626 | (async () => { 1627 | try { 1628 | if (message.action === 'showFloatingPanel') { 1629 | showFloatingPanel(); 1630 | sendResponse({ success: true }); 1631 | } else if (message.action === 'hideFloatingPanel') { 1632 | hideFloatingPanel(); 1633 | sendResponse({ success: true }); 1634 | } else if (message.action === 'updateFloatingStatus') { 1635 | updateFloatingStatus(message.data.message, message.data.type); 1636 | sendResponse({ success: true }); 1637 | } else if (message.action === 'addFloatingLog') { 1638 | addFloatingLog(message.data.message, message.data.type); 1639 | sendResponse({ success: true }); 1640 | } else if (message.action === 'startRegistration') { 1641 | showFloatingPanel(); 1642 | updateFloatingStatus('开始注册...', 'info'); 1643 | addFloatingLog('开始注册流程', 'info'); 1644 | await handleStartRegistration(message.data); 1645 | updateFloatingStatus('注册流程已启动', 'success'); 1646 | addFloatingLog('注册流程已启动', 'success'); 1647 | sendResponse({ success: true, message: '注册流程已启动' }); 1648 | } else if (message.action === 'autoFillCard') { 1649 | showFloatingPanel(); 1650 | updateFloatingStatus('开始填卡...', 'info'); 1651 | addFloatingLog('开始填卡流程', 'info'); 1652 | await handleAutoFillCard(); 1653 | updateFloatingStatus('填卡流程已启动', 'success'); 1654 | addFloatingLog('填卡流程已启动', 'success'); 1655 | sendResponse({ success: true, message: '填卡流程已启动' }); 1656 | } else if (message.action === 'fillBankCard') { 1657 | showFloatingPanel(); 1658 | updateFloatingStatus('开始填写银行卡...', 'info'); 1659 | addFloatingLog('开始填写银行卡', 'info'); 1660 | await handleFillBankCard(); 1661 | updateFloatingStatus('银行卡填写已启动', 'success'); 1662 | addFloatingLog('银行卡填写已启动', 'success'); 1663 | sendResponse({ success: true, message: '银行卡填写已启动' }); 1664 | } else { 1665 | sendResponse({ success: false, message: '未知操作: ' + message.action }); 1666 | } 1667 | } catch (error) { 1668 | console.error('[Content Script] 处理消息失败:', error); 1669 | sendResponse({ success: false, message: error.message }); 1670 | } 1671 | })(); 1672 | 1673 | // 返回 true 表示异步响应 1674 | return true; 1675 | }); 1676 | 1677 | /** 1678 | * 生成随机名字 1679 | * @returns {Object} 包含 firstName 和 lastName 的对象 1680 | */ 1681 | function generateRandomName() { 1682 | const firstNames = [ 1683 | 'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 1684 | 'Thomas', 'Charles', 'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 1685 | 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua', 'Kenneth', 'Kevin', 'Brian', 1686 | 'George', 'Edward', 'Ronald', 'Timothy', 'Jason', 'Jeffrey', 'Ryan', 'Jacob', 1687 | 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 1688 | 'Brandon', 'Benjamin', 'Samuel', 'Frank', 'Gregory', 'Raymond', 'Alexander', 1689 | 'Patrick', 'Jack', 'Dennis', 'Jerry', 'Tyler', 'Aaron', 'Jose', 'Henry', 1690 | 'Adam', 'Douglas', 'Nathan', 'Zachary', 'Kyle', 'Noah', 'Ethan', 'Jeremy', 1691 | 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 1692 | 'Jessica', 'Sarah', 'Karen', 'Nancy', 'Lisa', 'Betty', 'Margaret', 'Sandra', 1693 | 'Ashley', 'Kimberly', 'Emily', 'Donna', 'Michelle', 'Dorothy', 'Carol', 1694 | 'Amanda', 'Melissa', 'Deborah', 'Stephanie', 'Rebecca', 'Sharon', 'Laura', 1695 | 'Cynthia', 'Kathleen', 'Amy', 'Shirley', 'Angela', 'Helen', 'Anna', 'Brenda', 1696 | 'Pamela', 'Nicole', 'Emma', 'Samantha', 'Katherine', 'Christine', 'Debra', 1697 | 'Rachel', 'Carolyn', 'Janet', 'Virginia', 'Maria', 'Heather', 'Diane', 1698 | 'Julie', 'Joyce', 'Victoria', 'Kelly', 'Christina', 'Joan', 'Evelyn' 1699 | ]; 1700 | 1701 | const lastNames = [ 1702 | 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 1703 | 'Rodriguez', 'Martinez', 'Hernandez', 'Lopez', 'Wilson', 'Anderson', 'Thomas', 1704 | 'Taylor', 'Moore', 'Jackson', 'Martin', 'Lee', 'Thompson', 'White', 'Harris', 1705 | 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson', 'Walker', 'Young', 'Allen', 1706 | 'King', 'Wright', 'Scott', 'Torres', 'Nguyen', 'Hill', 'Flores', 'Green', 1707 | 'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera', 'Campbell', 'Mitchell', 'Carter', 1708 | 'Roberts', 'Gomez', 'Phillips', 'Evans', 'Turner', 'Diaz', 'Parker', 'Cruz', 1709 | 'Edwards', 'Collins', 'Reyes', 'Stewart', 'Morris', 'Rogers', 'Reed', 'Cook', 1710 | 'Morgan', 'Bell', 'Murphy', 'Bailey', 'Rivera', 'Cooper', 'Richardson', 'Cox', 1711 | 'Howard', 'Ward', 'Torres', 'Peterson', 'Gray', 'Ramirez', 'James', 'Watson', 1712 | 'Brooks', 'Kelly', 'Sanders', 'Price', 'Bennett', 'Wood', 'Barnes', 'Ross', 1713 | 'Henderson', 'Coleman', 'Jenkins', 'Perry', 'Powell', 'Long', 'Patterson', 'Hughes' 1714 | ]; 1715 | 1716 | const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]; 1717 | const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]; 1718 | 1719 | return { firstName, lastName }; 1720 | } 1721 | 1722 | async function handleStartRegistration(data) { 1723 | console.log('[Content Script] 开始注册:', data); 1724 | 1725 | // 保存当前 URL,用于检测页面是否跳转 1726 | const currentUrl = location.href; 1727 | 1728 | try { 1729 | const { email, password, moemailId } = data || {}; 1730 | 1731 | if (moemailId && email) { 1732 | storeMoemailSession({ id: moemailId, email }); 1733 | console.log('[Content Script] 已记录 Moemail 邮箱:', email, moemailId); 1734 | startMoemailVerificationFlow({ forceStart: true }); 1735 | } else { 1736 | clearMoemailSession(); 1737 | } 1738 | 1739 | if (!email || !password) { 1740 | throw new Error('邮箱或密码为空'); 1741 | } 1742 | 1743 | // 生成随机名字 1744 | const { firstName, lastName } = generateRandomName(); 1745 | console.log('[Content Script] 生成的名字:', firstName, lastName); 1746 | 1747 | // 等待页面元素加载 1748 | await new Promise(resolve => setTimeout(resolve, 500)); 1749 | 1750 | // 1. 填写 First name(名) 1751 | const firstNameSelectors = [ 1752 | 'input[name*="first" i]', 1753 | 'input[id*="first" i]', 1754 | 'input[placeholder*="first name" i]', 1755 | 'input[placeholder*="First name" i]', 1756 | 'input[autocomplete*="given-name" i]' 1757 | ]; 1758 | 1759 | let firstNameInput = null; 1760 | for (const selector of firstNameSelectors) { 1761 | firstNameInput = document.querySelector(selector); 1762 | if (firstNameInput) break; 1763 | } 1764 | 1765 | if (firstNameInput) { 1766 | firstNameInput.focus(); 1767 | firstNameInput.value = ''; 1768 | firstNameInput.value = firstName; 1769 | firstNameInput.dispatchEvent(new Event('input', { bubbles: true })); 1770 | firstNameInput.dispatchEvent(new Event('change', { bubbles: true })); 1771 | console.log('[Content Script] 已填写 First name:', firstName); 1772 | } else { 1773 | console.warn('[Content Script] 未找到 First name 输入框'); 1774 | } 1775 | 1776 | // 2. 填写 Last name(姓) 1777 | const lastNameSelectors = [ 1778 | 'input[name*="last" i]', 1779 | 'input[id*="last" i]', 1780 | 'input[placeholder*="last name" i]', 1781 | 'input[placeholder*="Last name" i]', 1782 | 'input[autocomplete*="family-name" i]' 1783 | ]; 1784 | 1785 | let lastNameInput = null; 1786 | for (const selector of lastNameSelectors) { 1787 | lastNameInput = document.querySelector(selector); 1788 | if (lastNameInput) break; 1789 | } 1790 | 1791 | if (lastNameInput) { 1792 | lastNameInput.focus(); 1793 | lastNameInput.value = ''; 1794 | lastNameInput.value = lastName; 1795 | lastNameInput.dispatchEvent(new Event('input', { bubbles: true })); 1796 | lastNameInput.dispatchEvent(new Event('change', { bubbles: true })); 1797 | console.log('[Content Script] 已填写 Last name:', lastName); 1798 | } else { 1799 | console.warn('[Content Script] 未找到 Last name 输入框'); 1800 | } 1801 | 1802 | // 3. 填写邮箱 1803 | const emailSelectors = [ 1804 | 'input[type="email"]', 1805 | 'input[name*="email" i]', 1806 | 'input[id*="email" i]', 1807 | 'input[placeholder*="email" i]', 1808 | 'input[autocomplete*="email" i]' 1809 | ]; 1810 | 1811 | let emailInput = null; 1812 | for (const selector of emailSelectors) { 1813 | try { 1814 | emailInput = await waitForElement(selector, 2000); 1815 | if (emailInput) break; 1816 | } catch (e) { 1817 | // 继续尝试下一个选择器 1818 | } 1819 | } 1820 | 1821 | if (!emailInput) { 1822 | emailInput = document.querySelector('input[type="email"]') || 1823 | document.querySelector('input[name*="email" i]') || 1824 | document.querySelector('input[id*="email" i]'); 1825 | } 1826 | 1827 | if (emailInput && email) { 1828 | emailInput.focus(); 1829 | emailInput.value = ''; 1830 | emailInput.value = email; 1831 | emailInput.dispatchEvent(new Event('input', { bubbles: true })); 1832 | emailInput.dispatchEvent(new Event('change', { bubbles: true })); 1833 | console.log('[Content Script] 已填写邮箱:', email); 1834 | } else { 1835 | console.warn('[Content Script] 未找到邮箱输入框'); 1836 | } 1837 | 1838 | // 4. 保存密码到 sessionStorage,供下一个页面使用 1839 | if (password) { 1840 | sessionStorage.setItem('windsurf_registration_password', password); 1841 | console.log('[Content Script] 密码已保存到 sessionStorage,将在下一个页面填写'); 1842 | } 1843 | 1844 | // 5. 勾选协议复选框 1845 | const checkboxSelectors = [ 1846 | 'input[type="checkbox"][name*="terms" i]', 1847 | 'input[type="checkbox"][id*="terms" i]', 1848 | 'input[type="checkbox"][name*="agreement" i]', 1849 | 'input[type="checkbox"][id*="agreement" i]', 1850 | 'input[type="checkbox"][name*="policy" i]', 1851 | 'input[type="checkbox"][id*="policy" i]', 1852 | 'input[type="checkbox"][name*="accept" i]', 1853 | 'input[type="checkbox"][id*="accept" i]', 1854 | 'input[type="checkbox"][aria-label*="terms" i]', 1855 | 'input[type="checkbox"][aria-label*="agreement" i]' 1856 | ]; 1857 | 1858 | let checkbox = null; 1859 | for (const selector of checkboxSelectors) { 1860 | checkbox = document.querySelector(selector); 1861 | if (checkbox) break; 1862 | } 1863 | 1864 | // 如果没找到,尝试查找所有复选框,选择第一个未选中的 1865 | if (!checkbox) { 1866 | const allCheckboxes = document.querySelectorAll('input[type="checkbox"]'); 1867 | for (const cb of allCheckboxes) { 1868 | // 检查复选框是否与协议相关(通过附近的文本) 1869 | const label = cb.closest('label') || 1870 | (cb.parentElement && cb.parentElement.textContent) || 1871 | (cb.nextSibling && cb.nextSibling.textContent); 1872 | const labelText = label ? (label.textContent || label.innerText || '').toLowerCase() : ''; 1873 | 1874 | if (labelText.includes('terms') || 1875 | labelText.includes('agreement') || 1876 | labelText.includes('policy') || 1877 | labelText.includes('privacy') || 1878 | labelText.includes('服务') || 1879 | labelText.includes('协议')) { 1880 | checkbox = cb; 1881 | break; 1882 | } 1883 | } 1884 | 1885 | // 如果还是没找到,使用第一个未选中的复选框 1886 | if (!checkbox && allCheckboxes.length > 0) { 1887 | for (const cb of allCheckboxes) { 1888 | if (!cb.checked) { 1889 | checkbox = cb; 1890 | break; 1891 | } 1892 | } 1893 | } 1894 | } 1895 | 1896 | if (checkbox) { 1897 | if (!checkbox.checked) { 1898 | checkbox.click(); 1899 | checkbox.checked = true; 1900 | checkbox.dispatchEvent(new Event('change', { bubbles: true })); 1901 | checkbox.dispatchEvent(new Event('click', { bubbles: true })); 1902 | console.log('[Content Script] 已勾选协议复选框'); 1903 | } else { 1904 | console.log('[Content Script] 协议复选框已勾选'); 1905 | } 1906 | } else { 1907 | console.warn('[Content Script] 未找到协议复选框'); 1908 | } 1909 | 1910 | // 等待一下让页面处理所有输入和验证 1911 | await new Promise(resolve => setTimeout(resolve, 1000)); 1912 | 1913 | // 6. 查找并点击提交按钮 1914 | console.log('[Content Script] 开始查找提交按钮...'); 1915 | 1916 | // 辅助函数:检查元素是否可见 1917 | function isElementVisible(element) { 1918 | if (!element) return false; 1919 | const style = window.getComputedStyle(element); 1920 | return style.display !== 'none' && 1921 | style.visibility !== 'hidden' && 1922 | style.opacity !== '0' && 1923 | element.offsetWidth > 0 && 1924 | element.offsetHeight > 0; 1925 | } 1926 | 1927 | // 首先尝试通过 type="submit" 查找 1928 | let submitButton = document.querySelector('button[type="submit"]') || 1929 | document.querySelector('input[type="submit"]'); 1930 | 1931 | // 如果没找到,尝试通过文本内容查找 1932 | if (!submitButton || !isElementVisible(submitButton)) { 1933 | const allButtons = document.querySelectorAll('button, input[type="submit"], a[role="button"]'); 1934 | const submitTexts = ['Sign up', 'Create', 'Register', 'Continue', 'Next', 'Submit', 'Get started', '开始', '注册', '创建']; 1935 | 1936 | for (const btn of allButtons) { 1937 | if (!isElementVisible(btn)) continue; 1938 | 1939 | const btnText = (btn.textContent || btn.innerText || btn.value || '').trim(); 1940 | const btnAriaLabel = (btn.getAttribute('aria-label') || '').trim(); 1941 | const combinedText = (btnText + ' ' + btnAriaLabel).toLowerCase(); 1942 | 1943 | // 检查按钮文本是否包含提交相关的关键词 1944 | for (const text of submitTexts) { 1945 | if (combinedText.includes(text.toLowerCase())) { 1946 | // 排除明显不是提交按钮的文本(如 "Other Sign up options") 1947 | if (!combinedText.includes('other') && 1948 | !combinedText.includes('option') && 1949 | !combinedText.includes('link')) { 1950 | submitButton = btn; 1951 | console.log('[Content Script] 通过文本找到提交按钮:', btnText || btnAriaLabel); 1952 | break; 1953 | } 1954 | } 1955 | } 1956 | if (submitButton) break; 1957 | } 1958 | } 1959 | 1960 | // 如果还是没找到,尝试查找包含特定 class 或 id 的按钮 1961 | if (!submitButton || !isElementVisible(submitButton)) { 1962 | const classSelectors = [ 1963 | 'button[class*="submit" i]', 1964 | 'button[class*="primary" i]', 1965 | 'button[class*="continue" i]', 1966 | 'button[class*="next" i]', 1967 | 'button[id*="submit" i]', 1968 | 'button[id*="continue" i]', 1969 | 'button[id*="next" i]' 1970 | ]; 1971 | 1972 | for (const selector of classSelectors) { 1973 | submitButton = document.querySelector(selector); 1974 | if (submitButton && isElementVisible(submitButton)) { 1975 | console.log('[Content Script] 通过选择器找到提交按钮:', selector); 1976 | break; 1977 | } 1978 | } 1979 | } 1980 | 1981 | // 如果找到按钮但被禁用,等待一下再试 1982 | if (submitButton && submitButton.disabled) { 1983 | console.log('[Content Script] 提交按钮被禁用,等待表单验证...'); 1984 | await new Promise(resolve => setTimeout(resolve, 1500)); 1985 | 1986 | // 再次检查按钮是否可用 1987 | if (submitButton.disabled) { 1988 | console.warn('[Content Script] 提交按钮仍然被禁用,尝试强制点击'); 1989 | } 1990 | } 1991 | 1992 | if (submitButton && isElementVisible(submitButton)) { 1993 | // 尝试多种点击方式 1994 | try { 1995 | // 先聚焦 1996 | submitButton.focus(); 1997 | await new Promise(resolve => setTimeout(resolve, 200)); 1998 | 1999 | // 触发点击事件 2000 | submitButton.click(); 2001 | console.log('[Content Script] 已点击提交按钮,等待页面跳转...'); 2002 | 2003 | // 如果 click() 没有效果,尝试 dispatchEvent 2004 | setTimeout(() => { 2005 | if (location.href === currentUrl) { 2006 | console.log('[Content Script] 页面未跳转,尝试使用 dispatchEvent'); 2007 | const clickEvent = new MouseEvent('click', { 2008 | bubbles: true, 2009 | cancelable: true, 2010 | view: window 2011 | }); 2012 | submitButton.dispatchEvent(clickEvent); 2013 | } 2014 | }, 500); 2015 | } catch (e) { 2016 | console.error('[Content Script] 点击按钮失败:', e); 2017 | } 2018 | 2019 | // 重置标志,准备处理下一个页面 2020 | continueButtonClicked = false; 2021 | passwordFilled = false; 2022 | passwordSubmitted = false; 2023 | 2024 | // 监听页面跳转,在下一个页面自动填写密码 2025 | setTimeout(() => { 2026 | checkAndFillPassword(); 2027 | }, 2000); 2028 | setTimeout(() => { 2029 | checkAndFillPassword(); 2030 | }, 4000); 2031 | } else { 2032 | // 尝试查找表单并提交 2033 | const form = document.querySelector('form'); 2034 | if (form) { 2035 | console.log('[Content Script] 未找到提交按钮,尝试提交表单'); 2036 | form.submit(); 2037 | console.log('[Content Script] 已提交表单,等待页面跳转...'); 2038 | 2039 | // 重置标志,准备处理下一个页面 2040 | continueButtonClicked = false; 2041 | passwordFilled = false; 2042 | passwordSubmitted = false; 2043 | 2044 | // 监听页面跳转,在下一个页面自动填写密码 2045 | setTimeout(() => { 2046 | checkAndFillPassword(); 2047 | }, 2000); 2048 | setTimeout(() => { 2049 | checkAndFillPassword(); 2050 | }, 4000); 2051 | } else { 2052 | console.warn('[Content Script] 未找到提交按钮或表单'); 2053 | console.log('[Content Script] 所有按钮:', Array.from(document.querySelectorAll('button')).map(b => ({ 2054 | text: b.textContent?.trim(), 2055 | type: b.type, 2056 | disabled: b.disabled, 2057 | visible: isElementVisible(b) 2058 | }))); 2059 | } 2060 | } 2061 | 2062 | } catch (error) { 2063 | console.error('[Content Script] 注册处理失败:', error); 2064 | throw error; 2065 | } 2066 | } 2067 | 2068 | 2069 | /** 2070 | * 等待元素出现 2071 | * @param {string} selector - CSS 选择器 2072 | * @param {number} timeout - 超时时间(毫秒) 2073 | * @returns {Promise} 2074 | */ 2075 | function waitForElement(selector, timeout = 5000) { 2076 | return new Promise((resolve, reject) => { 2077 | const element = document.querySelector(selector); 2078 | if (element) { 2079 | resolve(element); 2080 | return; 2081 | } 2082 | 2083 | const observer = new MutationObserver((mutations, obs) => { 2084 | const element = document.querySelector(selector); 2085 | if (element) { 2086 | obs.disconnect(); 2087 | resolve(element); 2088 | } 2089 | }); 2090 | 2091 | observer.observe(document.body, { 2092 | childList: true, 2093 | subtree: true 2094 | }); 2095 | 2096 | setTimeout(() => { 2097 | observer.disconnect(); 2098 | reject(new Error('等待元素超时: ' + selector)); 2099 | }, timeout); 2100 | }); 2101 | } 2102 | 2103 | /** 2104 | * 处理自动填卡 2105 | */ 2106 | async function handleAutoFillCard() { 2107 | console.log('[Content Script] 开始自动填卡'); 2108 | 2109 | try { 2110 | // 这里应该包含填卡的具体逻辑 2111 | // 由于原代码混淆严重,这里提供一个基础框架 2112 | 2113 | // 查找卡号输入框 2114 | const cardNumberInput = document.querySelector('input[name*="card" i], input[id*="card" i], input[placeholder*="card" i], input[type="text"][autocomplete*="cc-number" i]'); 2115 | if (cardNumberInput) { 2116 | // 这里应该填入卡号(需要从某个地方获取) 2117 | console.log('[Content Script] 找到卡号输入框'); 2118 | // cardNumberInput.value = '卡号'; 2119 | } 2120 | 2121 | // 查找其他相关输入框(CVV、过期日期等) 2122 | // ... 2123 | 2124 | console.log('[Content Script] 自动填卡完成'); 2125 | } catch (error) { 2126 | console.error('[Content Script] 自动填卡失败:', error); 2127 | throw error; 2128 | } 2129 | } 2130 | 2131 | /** 2132 | * 处理银行卡填写 2133 | */ 2134 | async function handleFillBankCard() { 2135 | console.log('[Content Script] 开始填写银行卡'); 2136 | // 类似 handleAutoFillCard 的逻辑 2137 | await handleAutoFillCard(); 2138 | } 2139 | 2140 | --------------------------------------------------------------------------------