diff --git a/mv2_simple_crx/src/actions/amazon.js b/mv2_simple_crx/src/actions/amazon.js index 1feaf04..a62e067 100644 --- a/mv2_simple_crx/src/actions/amazon.js +++ b/mv2_simple_crx/src/actions/amazon.js @@ -1,10 +1,10 @@ // amazon_top_list:Amazon TOP 榜单抓取(Best Sellers / New Releases / Movers & Shakers) -import { openTab } from '../libs/tabs.js'; -import { execute_script } from '../libs/inject.js'; +import { create_tab_task } from '../libs/tabs.js'; +import { injected_amazon_search_list } from '../injected/amazon_search_list.js'; export function amazon_search_list(data, sendResponse) { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const category_keyword = (data && data.category_keyword) ? String(data.category_keyword).trim() : ''; const keyword = category_keyword || '野餐包'; @@ -24,159 +24,40 @@ export function amazon_search_list(data, sendResponse) { let times = 0; - openTab({ - url, - latest: false, - top: 20, - left: 20, - width: 1440, - height: 900, - target: '__amazon_search_list', + const send_action = (action, payload) => { + if (typeof sendResponse === 'function') { + sendResponse({ action, data: payload }); + sendResponse.log && sendResponse.log(payload); + } + }; - tabError(tab, details) { - const result = { - code: 30, - status: false, - message: (details && (details.message || details.statusLine || details.error)) || 'tab error', - data: null, - documentURI: details && details.url, - }; + const tab_task = create_tab_task(url) + .set_latest(false) + .set_bounds({ top: 20, left: 20, width: 1440, height: 900 }) + .set_target('__amazon_search_list'); - sendResponse && sendResponse({ action: 'amazon_search_list', data: result }); - sendResponse && sendResponse.log && sendResponse.log(result); + try { + const tab = await tab_task.open_async(); - if (tab && tab.remove) { - tab.remove(1500); - } + await tab.execute_script(injected_amazon_search_list, [{ url, category_keyword }], 'document_idle'); - reject(new Error(result.message)); - }, + const result = { code: 0, status: true, message: 'ok', data: { tab_id: tab.id, url, category_keyword } }; + send_action('amazon_search_list', result); + resolve({ tab_id: tab.id, url, category_keyword }); - tabUpdated(tab, details) { - if (times > 0) return; - times += 1; - - const tab_id = tab && tab.id; - if (!tab_id) { - return reject(new Error('tab.id 为空')); - } - - execute_script( - tab_id, - { - fn: (params) => { - const action = 'amazon_search_list'; - const start_url = params && params.url ? String(params.url) : location.href; - const category_keyword = params && params.category_keyword ? String(params.category_keyword).trim() : ''; - - function send(action_name, data) { - try { - chrome.runtime.sendMessage({ type: 'push', action: action_name, data }); - } catch (e) { - // ignore - } - } - - function abs_url(href) { - try { - return new URL(href, location.origin).toString(); - } catch (_) { - return href; - } - } - - function parse_asin_from_url(url) { - if (!url || typeof url !== 'string') return null; - const m = url.match(/\/dp\/([A-Z0-9]{10})/i) || url.match(/\/gp\/product\/([A-Z0-9]{10})/i); - return m ? m[1].toUpperCase() : null; - } - - function extract_results() { - const items = []; - const nodes = document.querySelectorAll('div.s-main-slot div[data-component-type="s-search-result"]'); - nodes.forEach((el, idx) => { - const asin = (el.getAttribute('data-asin') || '').trim() || null; - - const title_el = el.querySelector('h2 span') || el.querySelector('h2'); - const title = title_el ? title_el.textContent.trim() : null; - - // 搜索页有的标题在 h2 里,不一定包在 里;链接一般在 a[href*="/dp/"] - const a = el.querySelector('a[href*="/dp/"], a[href*="/gp/product/"]'); - const href = a ? a.getAttribute('href') : null; - const url = href ? abs_url(href) : null; - - - const price_el = el.querySelector('span.a-price > span.a-offscreen'); - const price = price_el ? price_el.textContent.trim() : null; - - const rating_el = el.querySelector('span.a-icon-alt'); - const rating_text = rating_el ? rating_el.textContent.trim() : null; - - // 评论数在不同语言/布局下 class 不稳定,这里做弱依赖 - const review_count_el = - el.querySelector('span[aria-label$="ratings"]') || - el.querySelector('span[aria-label$="rating"]') || - el.querySelector('span[aria-label$="评价"]') || - el.querySelector('span[aria-label$="评分"]'); - const review_count_text = review_count_el ? review_count_el.textContent.trim() : null; - - items.push({ - index: idx + 1, - asin: asin || parse_asin_from_url(url), - title, - url, - price, - rating_text, - review_count_text, - }); - }); - - return items; - } - - const items = extract_results(); - - send(action, { - stage: 'start', - start_url, - href: location.href, - category_keyword, - total: items.length, - }); - - // 不逐条推送,直接一次性推完整列表 - send(action, { - stage: 'list', - category_keyword, - total: items.length, - items, - }); - - send(action, { stage: 'end', category_keyword, total: items.length }); - }, - args: [{ url, category_keyword }], - }, - 'document_idle', - ) - .then(() => { - const result = { - code: 0, - status: true, - message: 'ok', - data: { tab_id, url, category_keyword }, - }; - sendResponse && sendResponse({ action: 'amazon_search_list', data: result }); - sendResponse && sendResponse.log && sendResponse.log(result); - resolve({ tab_id, url, category_keyword }); - }) - .catch((err) => { - if (tab && tab.remove) { - tab.remove(500); - } - reject(err); - }); - }, - }); + // 可选:自动关 tab(默认不关,方便调试) + // tab && tab.remove && tab.remove(3500); + } catch (err) { + const result = { + code: 30, + status: false, + message: (err && err.message) || String(err), + data: null, + documentURI: url, + }; + send_action('amazon_search_list', result); + reject(err); + } }); } diff --git a/mv2_simple_crx/src/background/index.js b/mv2_simple_crx/src/background/index.js index 3af9002..ba5cdb3 100644 --- a/mv2_simple_crx/src/background/index.js +++ b/mv2_simple_crx/src/background/index.js @@ -101,17 +101,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const action_send_response = create_action_send_response(sender); - Promise.resolve() - .then(() => fn(message.data || {}, action_send_response)) - .then((res) => { + (async () => { + try { + const res = await fn(message.data || {}, action_send_response); emit_ui_event('response', { type: 'response', request_id, ok: true, data: res, sender }); sendResponse({ ok: true, data: res, request_id }); - }) - .catch((err) => { + } catch (err) { const error = (err && err.message) || String(err); emit_ui_event('response', { type: 'response', request_id, ok: false, error, sender }); sendResponse({ ok: false, error, request_id }); - }); + } + })(); return true; }); diff --git a/mv2_simple_crx/src/content/content.js b/mv2_simple_crx/src/content/content.js index f07f7c7..e32c175 100644 --- a/mv2_simple_crx/src/content/content.js +++ b/mv2_simple_crx/src/content/content.js @@ -112,8 +112,9 @@ if (options.body !== undefined) detail.data = options.body; } - return target(...args) - .then(async (response) => { + return (async () => { + try { + const response = await target(...args); detail.ok = true; detail.message = 'ok'; const cloneResponse = response.clone(); @@ -122,7 +123,6 @@ detail.redirected = cloneResponse.redirected; detail.url = cloneResponse.url; detail.text = await cloneResponse.text(); - detail.response = response.clone(); detail.responseContentType = response.headers.get('Content-Type')?.toLowerCase(); if ( detail.responseContentType && @@ -131,14 +131,12 @@ detail.json = await response.clone().json(); } return response; - }) - .catch((error) => { + } catch (error) { detail.ok = false; detail.error = error; detail.message = error.message; throw error; - }) - .finally(() => { + } finally { const payload = { TAG: detail.TAG, method: detail.method, @@ -154,7 +152,8 @@ window.dispatchEvent(new CustomEvent('__REQUEST_DONE', { detail: payload })); detail = null; - }); + } + })(); }, }); } diff --git a/mv2_simple_crx/src/injected/amazon_search_list.js b/mv2_simple_crx/src/injected/amazon_search_list.js new file mode 100644 index 0000000..2bd50be --- /dev/null +++ b/mv2_simple_crx/src/injected/amazon_search_list.js @@ -0,0 +1,75 @@ +// 注入到页面的 Amazon 搜索列表解析逻辑 + +export function injected_amazon_search_list(params) { + const action = 'amazon_search_list'; + const start_url = params && params.url ? String(params.url) : location.href; + const category_keyword = params && params.category_keyword ? String(params.category_keyword).trim() : ''; + + function send(action_name, data) { + try { + chrome.runtime.sendMessage({ type: 'push', action: action_name, data }); + } catch (e) { + // ignore + } + } + + function abs_url(href) { + try { + return new URL(href, location.origin).toString(); + } catch (_) { + return href; + } + } + + function parse_asin_from_url(url) { + if (!url || typeof url !== 'string') return null; + const m = url.match(/\/dp\/([A-Z0-9]{10})/i) || url.match(/\/gp\/product\/([A-Z0-9]{10})/i); + return m ? m[1].toUpperCase() : null; + } + + function extract_results() { + const items = []; + const nodes = document.querySelectorAll('div.s-main-slot div[data-component-type="s-search-result"]'); + nodes.forEach((el, idx) => { + const asin = (el.getAttribute('data-asin') || '').trim() || null; + + const title_el = el.querySelector('h2 span') || el.querySelector('h2'); + const title = title_el ? title_el.textContent.trim() : null; + + const a = el.querySelector('a[href*="/dp/"], a[href*="/gp/product/"]'); + const href = a ? a.getAttribute('href') : null; + const url = href ? abs_url(href) : null; + + const price_el = el.querySelector('span.a-price > span.a-offscreen'); + const price = price_el ? price_el.textContent.trim() : null; + + const rating_el = el.querySelector('span.a-icon-alt'); + const rating_text = rating_el ? rating_el.textContent.trim() : null; + + const review_count_el = + el.querySelector('span[aria-label$="ratings"]') || + el.querySelector('span[aria-label$="rating"]') || + el.querySelector('span[aria-label$="评价"]') || + el.querySelector('span[aria-label$="评分"]'); + const review_count_text = review_count_el ? review_count_el.textContent.trim() : null; + + items.push({ + index: idx + 1, + asin: asin || parse_asin_from_url(url), + title, + url, + price, + rating_text, + review_count_text, + }); + }); + + return items; + } + + const items = extract_results(); + + send(action, { stage: 'start', start_url, href: location.href, category_keyword, total: items.length }); + send(action, { stage: 'list', category_keyword, total: items.length, items }); + send(action, { stage: 'end', category_keyword, total: items.length }); +} diff --git a/mv2_simple_crx/src/libs/inject.js b/mv2_simple_crx/src/libs/inject.js index 96c4a30..f9ef9e0 100644 --- a/mv2_simple_crx/src/libs/inject.js +++ b/mv2_simple_crx/src/libs/inject.js @@ -1,23 +1,19 @@ // executeScript:MV2 使用 chrome.tabs.executeScript -function normalize_code(code) { - // 支持:直接传函数 - if (typeof code === 'function') { - return `(${code.toString()})();`; +function build_code(fn, args) { + if (typeof fn === 'function') { + if (Array.isArray(args) && args.length) { + return `(${fn.toString()}).apply(null, ${JSON.stringify(args)});`; + } + return `(${fn.toString()})();`; } - - // 支持:传 { fn, args },这样 action 文件里不需要拼字符串也能传参 - if (code && typeof code === 'object' && typeof code.fn === 'function') { - const args = Array.isArray(code.args) ? code.args : []; - return `(${code.fn.toString()}).apply(null, ${JSON.stringify(args)});`; - } - - return code; + return fn; } -export function execute_script(tab_id, code, run_at) { +// execute_script(tabId, fn, args?, runAt?) +export function execute_script(tab_id, fn, args, run_at) { run_at = run_at || 'document_idle'; - code = normalize_code(code); + const code = build_code(fn, args); return new Promise((resolve, reject) => { chrome.tabs.executeScript( diff --git a/mv2_simple_crx/src/libs/tabs.js b/mv2_simple_crx/src/libs/tabs.js index 49cdd9e..dd00358 100644 --- a/mv2_simple_crx/src/libs/tabs.js +++ b/mv2_simple_crx/src/libs/tabs.js @@ -1,5 +1,7 @@ // openTab:MV2 版本(极简 + 回调风格) +import { execute_script } from './inject.js'; + function attach_tab_helpers(tab) { if (!tab) return tab; @@ -10,6 +12,10 @@ function attach_tab_helpers(tab) { }, Math.max(0, delay_ms)); }; + tab.execute_script = async function execute_script_on_tab(fn, args, run_at) { + return await execute_script(tab.id, fn, args, run_at); + }; + return tab; } @@ -55,24 +61,46 @@ export function close_tab(tab_id, delay_ms) { }, Math.max(0, delay_ms)); } -// 兼容你原来的调用风格:openTab({ url, ..., tabError(tab, details), tabUpdated(tab, details) }) -export function openTab(options) { - options = options && typeof options === 'object' ? options : {}; - const url = options.url; +// openTab 任务对象:用对象绑定方法,减少重复参数 +export function create_tab_task(url) { + const task = { + url, + latest: false, + top: 20, + left: 20, + width: 1440, + height: 900, + target: null, + active: true, + // 你期望的写法:tab_task.on_updated = () => {} + on_error: null, + on_updated: null, + set_bounds(bounds) { + bounds = bounds && typeof bounds === 'object' ? bounds : {}; + if (Object.prototype.hasOwnProperty.call(bounds, 'top')) this.top = bounds.top; + if (Object.prototype.hasOwnProperty.call(bounds, 'left')) this.left = bounds.left; + if (Object.prototype.hasOwnProperty.call(bounds, 'width')) this.width = bounds.width; + if (Object.prototype.hasOwnProperty.call(bounds, 'height')) this.height = bounds.height; + return this; + }, + set_target(target) { + this.target = target || null; + return this; + }, + set_latest(latest) { + this.latest = !!latest; + return this; + }, + set_active(active) { + this.active = active !== false; + return this; + }, + async open_async() { + // 直接返回 tab 对象(带 remove / execute_script) + const { tab } = await open_tab(this.url, { active: this.active !== false }); + return tab; + }, + }; - const tabError = typeof options.tabError === 'function' ? options.tabError : () => void 0; - const tabUpdated = typeof options.tabUpdated === 'function' ? options.tabUpdated : () => void 0; - - if (!url) { - tabError(null, { error: 'url 不能为空' }); - return; - } - - open_tab(url, { active: options.active !== false }) - .then(({ tab }) => { - tabUpdated(tab, { status: 'complete' }); - }) - .catch((err) => { - tabError(null, { error: err.message || String(err) }); - }); + return task; } diff --git a/mv2_simple_crx/src/ui/index.html b/mv2_simple_crx/src/ui/index.html index 8f2c789..2359984 100644 --- a/mv2_simple_crx/src/ui/index.html +++ b/mv2_simple_crx/src/ui/index.html @@ -44,11 +44,6 @@
响应

         
-
-        
-
推送事件(实时)
-

-        
diff --git a/mv2_simple_crx/src/ui/index.js b/mv2_simple_crx/src/ui/index.js index 81fc7c8..7624d06 100644 --- a/mv2_simple_crx/src/ui/index.js +++ b/mv2_simple_crx/src/ui/index.js @@ -4,8 +4,8 @@ const btn_run_el = document.getElementById('btn_run'); const btn_clear_el = document.getElementById('btn_clear'); const btn_bg_reload_el = document.getElementById('btn_bg_reload'); const last_response_el = document.getElementById('last_response'); -const event_log_el = document.getElementById('event_log'); let actions_meta = {}; +const ui_state = { events: [] }; function now_time() { const d = new Date(); @@ -25,34 +25,40 @@ function set_last_response(obj) { last_response_el.textContent = JSON.stringify(obj, null, 2); } -function append_event_line(obj) { - const line = `[${now_time()}] ${JSON.stringify(obj)}`; - event_log_el.textContent = (event_log_el.textContent ? event_log_el.textContent + '\n' : '') + line; - event_log_el.scrollTop = event_log_el.scrollHeight; +function render_state() { + // 只在一个窗口展示所有数据,方便查看 + last_response_el.textContent = JSON.stringify(ui_state, null, 2); +} + +function push_event(obj) { + ui_state.events.push({ ts: now_time(), ...obj }); + // 简单限长,避免无限增长 + if (ui_state.events.length > 300) { + ui_state.events.splice(0, ui_state.events.length - 300); + } + render_state(); } btn_run_el.addEventListener('click', () => { const action = action_name_el.value; const params = safe_json_parse(action_params_el.value || '{}'); - append_event_line({ type: 'call', action, params }); - set_last_response({ running: true, action, params }); + push_event({ type: 'call', action, params }); chrome.runtime.sendMessage({ action, data: params }, (res) => { - set_last_response(res); - append_event_line({ type: 'response', action, res }); + push_event({ type: 'reply', action, res }); }); }); btn_clear_el.addEventListener('click', () => { - last_response_el.textContent = ''; - event_log_el.textContent = ''; + ui_state.events = []; + render_state(); }); btn_bg_reload_el.addEventListener('click', () => { - append_event_line({ type: 'ui', action: 'reload_background' }); + push_event({ type: 'ui', action: 'reload_background' }); chrome.runtime.sendMessage({ action: 'reload_background', data: {} }, (res) => { - append_event_line({ type: 'ui', action: 'reload_background_done', res }); + push_event({ type: 'ui', action: 'reload_background_done', res }); }); }); @@ -103,17 +109,17 @@ chrome.runtime.onMessage.addListener((message) => { } if (message.event_name === 'push') { - append_event_line({ type: 'push', payload: message.payload }); + push_event({ type: 'push', payload: message.payload }); return; } if (message.event_name === 'request') { - append_event_line({ type: 'request', payload: message.payload }); + push_event({ type: 'request', payload: message.payload }); return; } if (message.event_name === 'response') { - append_event_line({ type: 'bg_response', payload: message.payload }); + push_event({ type: 'bg_response', payload: message.payload }); return; } }); @@ -126,4 +132,5 @@ chrome.runtime.sendMessage({ action: 'meta_actions', data: {} }, (res) => { } }); -append_event_line({ type: 'ready', hint: '点击右侧/上方执行按钮开始' }); +render_state(); +push_event({ type: 'ready', hint: '点击右侧/上方执行按钮开始' });