diff --git a/mv2_simple_crx/src/actions/amazon.js b/mv2_simple_crx/src/actions/amazon.js index 78d55eb..579996d 100644 --- a/mv2_simple_crx/src/actions/amazon.js +++ b/mv2_simple_crx/src/actions/amazon.js @@ -1,24 +1,121 @@ -// Amazon:action 壳(编排逻辑移至 amazon_tool.js) +// Amazon:直接在 handler 中编写逻辑,简化封装层级 +import { create_tab_task, ok_response, fail_response, guard_sync, response_code, injected_amazon_validate_captcha_continue, injected_amazon_product_detail, injected_amazon_product_reviews, normalize_product_url, pick_first_script_result, run_amazon_pdp_action_multi, injected_amazon_switch_language, injected_amazon_search_list, injected_amazon_homepage_search, sleep_ms, try_solve_amazon_validate_captcha, wait_until_search_list_url, get_tab_url } from './amazon_tool.js'; -import { injected_amazon_product_detail, injected_amazon_product_reviews, run_amazon_pdp_action, run_amazon_pdp_action_multi, run_amazon_search_list_action, run_amazon_set_language_action } from './amazon_tool.js'; +const AMAZON_HOME_FOR_LANG = 'https://www.amazon.com/customer-preferences/edit?ie=UTF8&preferencesReturnUrl=%2F&ref_=topnav_lang_ais&language=zh_CN¤cy=HKD'; +const AMAZON_ZH_HOME_URL = 'https://www.amazon.com/-/zh/ref=nav_logo'; -const amazon_search_list_action = { - desc: 'Amazon 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取', - params: { - category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' }, - sort_by: { type: 'string', desc: '排序方式:featured / price_asc / price_desc / review / newest / bestseller', default: 'featured' }, - limit: { type: 'number', desc: '抓取数量上限(默认 100,最大 200)', default: 100 }, - keep_tab_open: { type: 'boolean', desc: '调试用:不自动关闭窗口,方便手动刷新观察轨迹', default: false }, - }, - handler: run_amazon_search_list_action, +// 公共的 send_action 函数 +const create_send_action = (sendResponse) => (actionName, response) => { + sendResponse({ action: actionName, ...response }); }; export const amazon_actions = [ { name: 'amazon_search_list', - desc: amazon_search_list_action.desc, - params: amazon_search_list_action.params, - handler: amazon_search_list_action.handler, + desc: 'Amazon 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取', + params: { + category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' }, + sort_by: { type: 'string', desc: '排序方式:featured / price_asc / price_desc / review / newest / bestseller', default: 'featured' }, + limit: { type: 'number', desc: '抓取数量上限(默认 100,最大 200)', default: 100 }, + keep_tab_open: { type: 'boolean', desc: '调试用:不自动关闭窗口,方便手动刷新观察轨迹', default: false }, + }, + handler: async (data, sendResponse) => { + const send_action = create_send_action(sendResponse); + + const category_keyword = data && data.category_keyword ? String(data.category_keyword).trim() : ''; + const sort_by = data && data.sort_by ? String(data.sort_by).trim() : ''; + const keep_tab_open = data && data.keep_tab_open === true; + const limit = (() => { + const n = data && Object.prototype.hasOwnProperty.call(data, 'limit') ? Number(data.limit) : 100; + if (!Number.isFinite(n)) return 100; + return Math.max(1, Math.min(200, Math.floor(n))); + })(); + const keyword = category_keyword || 'picnic bag'; + const sort_map = { + featured: 'relevanceblender', + review: 'review-rank', + newest: 'date-desc-rank', + price_asc: 'price-asc-rank', + price_desc: 'price-desc-rank', + bestseller: 'exact-aware-popularity-rank', + }; + const sort_s = Object.prototype.hasOwnProperty.call(sort_map, sort_by) ? sort_map[sort_by] : ''; + + const tab_task = create_tab_task(AMAZON_ZH_HOME_URL) + .set_latest(false) + .set_bounds({ top: 20, left: 20, width: 1440, height: 900 }) + .set_target('__amazon_search_list'); + let url = AMAZON_ZH_HOME_URL; + + try { + const tab = await tab_task.open_async(); + const payload = await tab.wait_update_complete_once(async () => { + await tab.execute_script(injected_amazon_search_list, [{ category_keyword, sort_by, debug: true }], 'document_idle'); + await try_solve_amazon_validate_captcha(tab, 3); + let home_ok = null; + for (let i = 0; i < 3; i += 1) { + const home_ret = await tab.execute_script(injected_amazon_homepage_search, [{ keyword }], 'document_idle'); + home_ok = pick_first_script_result(home_ret); + if (home_ok && home_ok.ok) break; + await sleep_ms(400); + await tab.wait_complete(); + await try_solve_amazon_validate_captcha(tab, 1); + } + if (!home_ok || !home_ok.ok) { + const current_url = await get_tab_url(tab.id).catch(() => ''); + const detail = home_ok && typeof home_ok === 'object' + ? JSON.stringify(home_ok) + : String(home_ok); + throw new Error(`首页搜索提交失败: ${detail}; url=${current_url || 'unknown'}`); + } + url = await wait_until_search_list_url(tab.id, 45000); + await tab.wait_complete(); + if (sort_s) { + const u = new URL(url); + u.searchParams.set('s', sort_s); + url = u.toString(); + await tab.navigate(url); + await tab.wait_complete(); + } + const unique_map = new Map(); + let next_url = url; + for (let page = 1; page <= 10 && unique_map.size < limit; page += 1) { + if (page > 1) { + await tab.navigate(next_url); + await tab.wait_complete(); + } + const injected_result_list = await tab.execute_script( + injected_amazon_search_list, + [{ url: next_url, category_keyword, sort_by }], + 'document_idle', + ); + const injected_result = pick_first_script_result(injected_result_list); + const items = injected_result && Array.isArray(injected_result.items) ? injected_result.items : []; + items.forEach((it) => { + const k = it && (it.asin || it.url) ? String(it.asin || it.url) : null; + if (!k) return; + if (!unique_map.has(k)) unique_map.set(k, it); + }); + if (unique_map.size >= limit) break; + next_url = injected_result && injected_result.next_url ? String(injected_result.next_url) : null; + if (!next_url) break; + } + const list_result = { stage: 'list', limit, total: unique_map.size, items: Array.from(unique_map.values()).slice(0, limit) }; + return { tab_id: tab.id, url, category_keyword, sort_by: sort_by || 'featured', limit, result: list_result }; + }); + + send_action('amazon_search_list', ok_response(payload)); + if (!keep_tab_open) tab.remove(0); + return payload; + } catch (err) { + send_action('amazon_search_list', fail_response((err && err.message) || String(err), { + code: response_code.runtime_error, + documentURI: url || AMAZON_ZH_HOME_URL, + })); + if (!keep_tab_open) tab.remove(0); + throw err; + } + }, }, { name: 'amazon_set_language', @@ -26,7 +123,56 @@ export const amazon_actions = [ params: { lang: { type: 'string', desc: 'EN / ES / AR / DE / HE / KO / PT / ZH_CN(默认) / ZH_TW', default: 'ZH_CN' }, }, - handler: run_amazon_set_language_action, + handler: async (data, sendResponse) => { + const send_action = create_send_action(sendResponse); + + const mapping = { + EN: 'en_US', + ES: 'es_US', + AR: 'ar_AE', + DE: 'de_DE', + HE: 'he_IL', + KO: 'ko_KR', + PT: 'pt_BR', + ZH_CN: 'zh_CN', + ZH_TW: 'zh_TW', + }; + const raw_lang = data && data.lang != null ? String(data.lang).trim().toUpperCase() : 'ZH_CN'; + const code = Object.prototype.hasOwnProperty.call(mapping, raw_lang) ? raw_lang : 'ZH_CN'; + + const tab_task = create_tab_task(AMAZON_HOME_FOR_LANG) + .set_latest(false) + .set_bounds({ top: 20, left: 20, width: 1280, height: 900 }); + + try { + const tab = await tab_task.open_async(); + const payload = await tab.wait_update_complete_once(async () => { + await tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle'); + const raw = await tab.execute_script(injected_amazon_switch_language, [{ lang: code }], 'document_idle'); + const inj = pick_first_script_result(raw); + if (!inj || !inj.ok) { + throw new Error((inj && inj.error) || 'switch_language_failed'); + } + const final_url = await new Promise((resolve_url, reject_url) => { + chrome.tabs.get(tab.id, (tab_info) => { + if (chrome.runtime.lastError) return reject_url(new Error(chrome.runtime.lastError.message)); + resolve_url(tab_info && tab_info.url ? tab_info.url : ''); + }); + }); + return { tab_id: tab.id, lang: inj.lang, url: final_url }; + }); + + send_action('amazon_set_language', ok_response(payload)); + tab.remove(0); + return payload; + } catch (err) { + send_action('amazon_set_language', fail_response((err && err.message) || String(err), { + code: response_code.runtime_error, + documentURI: AMAZON_HOME_FOR_LANG, + })); + throw err; + } + }, }, { name: 'amazon_product_detail', @@ -34,14 +180,40 @@ export const amazon_actions = [ params: { product_url: { type: 'string', desc: '商品详情页完整 URL(含 /dp/ASIN)', default: 'https://www.amazon.com/-/zh/dp/B0B56CHMSC' }, }, - handler: (data, sendResponse) => - run_amazon_pdp_action( - data && data.product_url, - injected_amazon_product_detail, - [], - 'amazon_product_detail', - sendResponse, - ), + handler: async (data, sendResponse) => { + const send_action = create_send_action(sendResponse); + + const normalized = guard_sync(() => normalize_product_url(data && data.product_url)); + if (!normalized.ok) { + send_action('amazon_product_detail', fail_response((normalized.error && normalized.error.message) || String(normalized.error), { + code: response_code.bad_request, + })); + return; + } + + const url = normalized.data; + const tab_task = create_tab_task(url).set_bounds({ top: 20, left: 20, width: 1280, height: 900 }); + + try { + const tab = await tab_task.open_async(); + const payload = await tab.wait_update_complete_once(async () => { + await tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle'); + const raw_list = await tab.execute_script(injected_amazon_product_detail, [], 'document_idle'); + const result = pick_first_script_result(raw_list); + return { tab_id: tab.id, product_url: url, result }; + }); + + send_action('amazon_product_detail', ok_response(payload)); + tab.remove(0); + return payload; + } catch (err) { + send_action('amazon_product_detail', fail_response((err && err.message) || String(err), { + code: response_code.runtime_error, + documentURI: url, + })); + throw err; + } + }, }, { name: 'amazon_product_reviews', @@ -50,14 +222,41 @@ export const amazon_actions = [ product_url: { type: 'string', desc: '商品详情页完整 URL', default: 'https://www.amazon.com/-/zh/dp/B0B56CHMSC' }, limit: { type: 'number', desc: '最多条数(默认 50,上限 100)', default: 50 }, }, - handler: (data, sendResponse) => - run_amazon_pdp_action( - data && data.product_url, - injected_amazon_product_reviews, - [{ limit: data && data.limit != null ? Number(data.limit) : 50 }], - 'amazon_product_reviews', - sendResponse, - ), + handler: async (data, sendResponse) => { + const send_action = create_send_action(sendResponse); + + const normalized = guard_sync(() => normalize_product_url(data && data.product_url)); + if (!normalized.ok) { + send_action('amazon_product_reviews', fail_response((normalized.error && normalized.error.message) || String(normalized.error), { + code: response_code.bad_request, + })); + return; + } + + const url = normalized.data; + const tab_task = create_tab_task(url).set_bounds({ top: 20, left: 20, width: 1280, height: 900 }); + + try { + const tab = await tab_task.open_async(); + const payload = await tab.wait_update_complete_once(async () => { + await tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle'); + const limit = data && data.limit != null ? Number(data.limit) : 50; + const raw_list = await tab.execute_script(injected_amazon_product_reviews, [{ limit }], 'document_idle'); + const result = pick_first_script_result(raw_list); + return { tab_id: tab.id, product_url: url, result }; + }); + + send_action('amazon_product_reviews', ok_response(payload)); + tab.remove(0); + return payload; + } catch (err) { + send_action('amazon_product_reviews', fail_response((err && err.message) || String(err), { + code: response_code.runtime_error, + documentURI: url, + })); + throw err; + } + }, }, { name: 'amazon_product_detail_reviews', @@ -67,15 +266,14 @@ export const amazon_actions = [ limit: { type: 'number', desc: '最多评论条数(默认 50,上限 100)', default: 50 }, skip_detail: { type: 'boolean', desc: '当日已拉过详情则跳过详情提取', default: false }, }, - handler: (data, sendResponse) => - run_amazon_pdp_action_multi( - data && data.product_url, - [ - ...(data && data.skip_detail === true ? [] : [{ name: 'detail', injected_fn: injected_amazon_product_detail, inject_args: [] }]), - { name: 'reviews', injected_fn: injected_amazon_product_reviews, inject_args: [{ limit: data && data.limit != null ? Number(data.limit) : 50 }] }, - ], - 'amazon_product_detail_reviews', - sendResponse, - ), + handler: (data, sendResponse) => run_amazon_pdp_action_multi( + data && data.product_url, + [ + ...(data && data.skip_detail === true ? [] : [{ name: 'detail', injected_fn: injected_amazon_product_detail, inject_args: [] }]), + { name: 'reviews', injected_fn: injected_amazon_product_reviews, inject_args: [{ limit: data && data.limit != null ? Number(data.limit) : 50 }] }, + ], + 'amazon_product_detail_reviews', + sendResponse, + ), }, ]; diff --git a/mv2_simple_crx/src/actions/amazon_tool.js b/mv2_simple_crx/src/actions/amazon_tool.js index 1c479bc..4cbaed9 100644 --- a/mv2_simple_crx/src/actions/amazon_tool.js +++ b/mv2_simple_crx/src/actions/amazon_tool.js @@ -54,6 +54,24 @@ export function sleep_ms(ms) { return new Promise((resolve) => setTimeout(resolve, Number.isFinite(t) ? Math.max(0, t) : 0)); } +function pick_first_script_result(raw_list) { + if (!Array.isArray(raw_list) || raw_list.length === 0) return null; + const first = raw_list[0]; + if (first && typeof first === 'object' && Object.prototype.hasOwnProperty.call(first, 'result')) { + return first.result; + } + return first; +} + +/** 由 background 传入的 sendResponse 生成统一推送(含可选 .log) */ +function create_send_action(sendResponse) { + return (action, payload) => { + if (typeof sendResponse !== 'function') return; + sendResponse({ action, data: payload }); + if (typeof sendResponse.log === 'function') sendResponse.log(payload); + }; +} + export async function try_solve_amazon_validate_captcha(tab, max_round) { const rounds = Number.isFinite(max_round) ? Math.max(1, Math.min(5, Math.floor(max_round))) : 2; for (let i = 0; i < rounds; i += 1) { @@ -99,26 +117,42 @@ export function injected_amazon_homepage_search(params) { 'input.nav-input[type="submit"]', ], 2000); if (btn) { - return { ok: dispatch_human_click(btn) }; + const clicked = dispatch_human_click(btn); + if (clicked) { + return { ok: true, method: 'button_click' }; + } } const form = input.form || input.closest('form'); if (form && typeof form.requestSubmit === 'function') { - form.requestSubmit(); - return { ok: true, method: 'request_submit' }; + try { + form.requestSubmit(); + return { ok: true, method: 'request_submit' }; + } catch (_) { + // continue fallback + } } if (form && typeof form.submit === 'function') { - form.submit(); - return { ok: true, method: 'form_submit' }; + try { + form.submit(); + return { ok: true, method: 'form_submit' }; + } catch (_) { + // continue fallback + } } try { + input.focus(); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); input.dispatchEvent(new KeyboardEvent('keypress', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); + // 兜底:直接派发 submit 事件 + if (form) { + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + } return { ok: true, method: 'keyboard_enter' }; } catch (_) { // ignore and return explicit error } - return { ok: false, error: 'no_submit', keyword }; + return { ok: false, error: 'submit_all_fallback_failed', keyword }; } export function injected_amazon_switch_language(params) { @@ -427,6 +461,15 @@ export function wait_until_search_list_url(tab_id, timeout_ms) { }); } +async function get_tab_url(tab_id) { + return await new Promise((resolve, reject) => { + chrome.tabs.get(tab_id, (tab) => { + if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message)); + resolve(tab && tab.url ? String(tab.url) : ''); + }); + }); +} + const AMAZON_ZH_HOME_URL = 'https://www.amazon.com/-/zh/ref=nav_logo'; const AMAZON_HOME_FOR_LANG = 'https://www.amazon.com/customer-preferences/edit?ie=UTF8&preferencesReturnUrl=%2F&ref_=topnav_lang_ais&language=zh_CN¤cy=HKD'; @@ -450,12 +493,7 @@ export async function run_amazon_search_list_action(data, sendResponse) { bestseller: 'exact-aware-popularity-rank', }; const sort_s = Object.prototype.hasOwnProperty.call(sort_map, sort_by) ? sort_map[sort_by] : ''; - const send_action = (action, payload) => { - if (typeof sendResponse === 'function') { - sendResponse({ action, data: payload }); - sendResponse.log && sendResponse.log(payload); - } - }; + const send_action = create_send_action(sendResponse); const tab_task = create_tab_task(AMAZON_ZH_HOME_URL) .set_latest(false) .set_bounds({ top: 20, left: 20, width: 1440, height: 900 }) @@ -471,10 +509,21 @@ export async function run_amazon_search_list_action(data, sendResponse) { const payload = await tab.wait_update_complete_once(async () => { await tab.execute_script(injected_amazon_search_list, [{ category_keyword, sort_by, debug: true }], 'document_idle'); await try_solve_amazon_validate_captcha(tab, 3); - const home_ret = await tab.execute_script(injected_amazon_homepage_search, [{ keyword }], 'document_idle'); - const home_ok = Array.isArray(home_ret) ? home_ret[0] : home_ret; + let home_ok = null; + for (let i = 0; i < 3; i += 1) { + const home_ret = await tab.execute_script(injected_amazon_homepage_search, [{ keyword }], 'document_idle'); + home_ok = pick_first_script_result(home_ret); + if (home_ok && home_ok.ok) break; + await sleep_ms(400); + await tab.wait_complete(); + await try_solve_amazon_validate_captcha(tab, 1); + } if (!home_ok || !home_ok.ok) { - throw new Error((home_ok && home_ok.error) || '首页搜索提交失败'); + const current_url = await get_tab_url(tab.id).catch(() => ''); + const detail = home_ok && typeof home_ok === 'object' + ? JSON.stringify(home_ok) + : String(home_ok); + throw new Error(`首页搜索提交失败: ${detail}; url=${current_url || 'unknown'}`); } url = await wait_until_search_list_url(tab.id, 45000); await tab.wait_complete(); @@ -497,7 +546,7 @@ export async function run_amazon_search_list_action(data, sendResponse) { [{ url: next_url, category_keyword, sort_by }], 'document_idle', ); - const injected_result = Array.isArray(injected_result_list) ? injected_result_list[0] : null; + const injected_result = pick_first_script_result(injected_result_list); const items = injected_result && Array.isArray(injected_result.items) ? injected_result.items : []; items.forEach((it) => { const k = it && (it.asin || it.url) ? String(it.asin || it.url) : null; @@ -537,12 +586,7 @@ export async function run_amazon_set_language_action(data, sendResponse) { }; const raw_lang = data && data.lang != null ? String(data.lang).trim().toUpperCase() : 'ZH_CN'; const code = Object.prototype.hasOwnProperty.call(mapping, raw_lang) ? raw_lang : 'ZH_CN'; - const send_action = (action, payload) => { - if (typeof sendResponse === 'function') { - sendResponse({ action, data: payload }); - sendResponse.log && sendResponse.log(payload); - } - }; + const send_action = create_send_action(sendResponse); const tab_task = create_tab_task(AMAZON_HOME_FOR_LANG) .set_latest(false) .set_bounds({ top: 20, left: 20, width: 1280, height: 900 }); @@ -556,7 +600,7 @@ export async function run_amazon_set_language_action(data, sendResponse) { const payload = await tab.wait_update_complete_once(async () => { await tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle'); const raw = await tab.execute_script(injected_amazon_switch_language, [{ lang: code }], 'document_idle'); - const inj = Array.isArray(raw) ? raw[0] : raw; + const inj = pick_first_script_result(raw); if (!inj || !inj.ok) { throw new Error((inj && inj.error) || 'switch_language_failed'); } @@ -580,12 +624,7 @@ export async function run_amazon_set_language_action(data, sendResponse) { } export async function run_amazon_pdp_action(product_url, injected_fn, inject_args, action_name, sendResponse) { - const send_action = (action, payload) => { - if (typeof sendResponse === 'function') { - sendResponse({ action, data: payload }); - sendResponse.log && sendResponse.log(payload); - } - }; + const send_action = create_send_action(sendResponse); const normalized = guard_sync(() => normalize_product_url(product_url)); if (!normalized.ok) { send_action(action_name, fail_response((normalized.error && normalized.error.message) || String(normalized.error), { @@ -606,7 +645,7 @@ export async function run_amazon_pdp_action(product_url, injected_fn, inject_arg await tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle'); await try_solve_amazon_validate_captcha(tab, 3); const raw_list = await tab.execute_script(injected_fn, inject_args || [], 'document_idle'); - const result = Array.isArray(raw_list) ? raw_list[0] : raw_list; + const result = pick_first_script_result(raw_list); return { tab_id: tab.id, product_url: url, result }; }).catch((err) => { send_action(action_name, fail_response((err && err.message) || String(err), { @@ -621,12 +660,7 @@ export async function run_amazon_pdp_action(product_url, injected_fn, inject_arg } export async function run_amazon_pdp_action_multi(product_url, steps, action_name, sendResponse) { - const send_action = (action, payload) => { - if (typeof sendResponse === 'function') { - sendResponse({ action, data: payload }); - sendResponse.log && sendResponse.log(payload); - } - }; + const send_action = create_send_action(sendResponse); const normalized = guard_sync(() => normalize_product_url(product_url)); if (!normalized.ok) { send_action(action_name, fail_response((normalized.error && normalized.error.message) || String(normalized.error), { @@ -650,7 +684,7 @@ export async function run_amazon_pdp_action_multi(product_url, steps, action_nam for (const step of steps || []) { if (!step || !step.name || typeof step.injected_fn !== 'function') continue; const raw_list = await tab.execute_script(step.injected_fn, step.inject_args || [], 'document_idle'); - const result = Array.isArray(raw_list) ? raw_list[0] : raw_list; + const result = pick_first_script_result(raw_list); results[step.name] = result; } return { tab_id: tab.id, product_url: url, result: results }; diff --git a/mv2_simple_crx/src/actions/index.js b/mv2_simple_crx/src/actions/index.js index e3373dc..d36f84c 100644 --- a/mv2_simple_crx/src/actions/index.js +++ b/mv2_simple_crx/src/actions/index.js @@ -7,9 +7,9 @@ import { amazon_actions } from './amazon.js'; export { amazon_actions }; -// Amazon 工具函数 -import { injected_amazon_validate_captcha_continue, is_amazon_validate_captcha_url, injected_amazon_product_detail, injected_amazon_product_reviews, run_amazon_pdp_action, run_amazon_pdp_action_multi, run_amazon_search_list_action, run_amazon_set_language_action } from './amazon_tool.js'; -export { injected_amazon_validate_captcha_continue, is_amazon_validate_captcha_url, injected_amazon_product_detail, injected_amazon_product_reviews, run_amazon_pdp_action, run_amazon_pdp_action_multi, run_amazon_search_list_action, run_amazon_set_language_action }; +// Amazon 工具函数(仅保留直接需要的) +import { injected_amazon_validate_captcha_continue, is_amazon_validate_captcha_url, injected_amazon_product_detail, injected_amazon_product_reviews, run_amazon_pdp_action_multi } from './amazon_tool.js'; +export { injected_amazon_validate_captcha_continue, is_amazon_validate_captcha_url, injected_amazon_product_detail, injected_amazon_product_reviews, run_amazon_pdp_action_multi }; // 便捷的统一导出对象 export const Actions = { diff --git a/mv2_simple_crx/src/background/index.js b/mv2_simple_crx/src/background/index.js index 8d925f9..c182a36 100644 --- a/mv2_simple_crx/src/background/index.js +++ b/mv2_simple_crx/src/background/index.js @@ -1,27 +1,18 @@ import { amazon_actions, getAllActionsMeta, getActionByName } from '../actions/index.js'; -// 调试日志系统 -const DEBUG = true; -const debug_log = (level, ...args) => { - if (DEBUG) { - const timestamp = new Date().toISOString(); - console[level](`[Background ${timestamp}]`, ...args); - } -}; - // action 注册表:供 UI 下拉选择 + server bridge 调用 let action_list = []; try { if (Array.isArray(amazon_actions)) { action_list = amazon_actions.filter(item => item && typeof item === 'object' && item.name); - debug_log('log', `Loaded ${action_list.length} actions:`, action_list.map(item => item.name)); + console.log(`Loaded ${action_list.length} actions:`, action_list.map(item => item.name)); } else { - debug_log('warn', 'amazon_actions is not an array:', amazon_actions); + console.warn('amazon_actions is not an array:', amazon_actions); } } catch (error) { - debug_log('error', 'Failed to load amazon_actions:', error); + console.error('Failed to load amazon_actions:', error); } const list_actions_meta = () => getAllActionsMeta(); @@ -38,6 +29,11 @@ const create_action_send_response = (sender) => { const ui_page_url = chrome.runtime.getURL('ui/index.html'); +const is_port_closed_error = (message) => { + const text = message ? String(message) : ''; + return text.includes('The message port closed before a response was received'); +}; + const emit_ui_event = (event_name, payload) => { try { chrome.runtime.sendMessage({ @@ -47,7 +43,10 @@ const emit_ui_event = (event_name, payload) => { ts: Date.now(), }, (response) => { if (chrome.runtime.lastError) { - console.warn('Failed to send UI event:', chrome.runtime.lastError.message); + const err_msg = chrome.runtime.lastError.message; + // UI 页面未打开/已关闭时属于正常场景,不打印告警噪音 + if (is_port_closed_error(err_msg)) return; + console.warn('Failed to send UI event:', err_msg); } }); } catch (error) { @@ -60,22 +59,22 @@ chrome.browserAction.onClicked.addListener(() => { }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - debug_log('log', 'Received message:', { message, sender: sender.tab?.url || 'background' }); + console.log('Received message:', { message, sender: sender.tab?.url || 'background' }); if (!message) { - debug_log('warn', 'Empty message received'); + console.warn('Empty message received'); return; } // UI 自己发出来的事件,background 不处理 if (message.channel === 'ui_event') { - debug_log('log', 'Ignoring ui_event message'); + console.log('Ignoring ui_event message'); return; } // content -> background 的推送消息(通用) if (message.type === 'push') { - debug_log('log', 'Processing push message:', message.action); + console.log('Processing push message:', message.action); emit_ui_event('push', { type: 'push', action: message.action, @@ -87,21 +86,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // UI -> background 的 action 调用 if (!message.action) { - debug_log('error', 'Missing action in message'); + console.error('Missing action in message'); sendResponse && sendResponse({ ok: false, error: '缺少 action' }); return; } // UI 获取 action 元信息(用于下拉/默认参数) if (message.action === 'meta_actions') { - debug_log('log', 'Returning actions meta'); + console.log('Returning actions meta'); sendResponse({ ok: true, data: list_actions_meta() }); return; } // UI 刷新后台(重启 background page) if (message.action === 'reload_background') { - debug_log('log', 'Reloading background page'); + console.log('Reloading background page'); sendResponse({ ok: true }); setTimeout(() => { location.reload(); @@ -112,20 +111,20 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const action_item = getActionByName(message.action); const action_handler = action_item && typeof action_item.handler === 'function' ? action_item.handler : null; if (!action_handler) { - debug_log('error', 'Unknown action:', message.action); + console.error('Unknown action:', message.action); sendResponse({ ok: false, error: '未知 action: ' + message.action }); return; } const request_id = `${Date.now()}_${Math.random().toString().slice(2)}`; - debug_log('log', 'Executing action:', { action: message.action, request_id, data: message.data }); + console.log('Executing action:', { action: message.action, request_id, data: message.data }); emit_ui_event('request', { type: 'request', request_id, action: message.action, data: message.data || {}, sender }); const action_send_response = create_action_send_response(sender); // 添加超时处理 const timeout = setTimeout(() => { - debug_log('warn', `Action ${message.action} timed out after 30000ms`); + console.warn(`Action ${message.action} timed out after 30000ms`); emit_ui_event('response', { type: 'response', request_id, @@ -140,14 +139,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { try { const res = await action_handler(message.data || {}, action_send_response); clearTimeout(timeout); - debug_log('log', `Action ${message.action} completed successfully:`, { request_id, result: res }); + console.log(`Action ${message.action} completed successfully:`, { request_id, result: res }); emit_ui_event('response', { type: 'response', request_id, ok: true, data: res, sender }); sendResponse({ ok: true, data: res, request_id }); } catch (err) { clearTimeout(timeout); const error = (err && err.message) || String(err); const stack = (err && err.stack) || ''; - debug_log('error', `Action ${message.action} failed:`, { error, stack, data: message.data }); + console.error(`Action ${message.action} failed:`, { error, stack, data: message.data }); emit_ui_event('response', { type: 'response', request_id, ok: false, error, stack, sender }); sendResponse({ ok: false, error, stack, request_id }); } diff --git a/mv2_simple_crx/src/ui/index.css b/mv2_simple_crx/src/ui/index.css index 9055c1a..4f6b796 100644 --- a/mv2_simple_crx/src/ui/index.css +++ b/mv2_simple_crx/src/ui/index.css @@ -159,6 +159,15 @@ body { overflow: auto; } +/* 响应区:不显示滚动条,长行自动换行(含无空格长串) */ +.pre_response { + overflow: visible; + overflow-x: hidden; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; +} + .pre_scroll { max-height: 520px; overflow: auto; diff --git a/mv2_simple_crx/src/ui/index.html b/mv2_simple_crx/src/ui/index.html index 0717010..42e67cd 100644 --- a/mv2_simple_crx/src/ui/index.html +++ b/mv2_simple_crx/src/ui/index.html @@ -20,6 +20,10 @@