This commit is contained in:
张成
2026-03-20 10:55:55 +08:00
parent 873fd436b1
commit 7ba462aedd
6 changed files with 352 additions and 108 deletions

View File

@@ -1,8 +1,17 @@
// Amazonaction 壳(编排逻辑移至 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&currency=HKD';
const AMAZON_ZH_HOME_URL = 'https://www.amazon.com/-/zh/ref=nav_logo';
const 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 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取', desc: 'Amazon 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取',
params: { params: {
category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' }, category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' },
@@ -10,15 +19,103 @@ const amazon_search_list_action = {
limit: { type: 'number', desc: '抓取数量上限(默认 100最大 200', default: 100 }, limit: { type: 'number', desc: '抓取数量上限(默认 100最大 200', default: 100 },
keep_tab_open: { type: 'boolean', desc: '调试用:不自动关闭窗口,方便手动刷新观察轨迹', default: false }, keep_tab_open: { type: 'boolean', desc: '调试用:不自动关闭窗口,方便手动刷新观察轨迹', default: false },
}, },
handler: run_amazon_search_list_action, handler: async (data, sendResponse) => {
}; const send_action = create_send_action(sendResponse);
export const amazon_actions = [ const category_keyword = data && data.category_keyword ? String(data.category_keyword).trim() : '';
{ const sort_by = data && data.sort_by ? String(data.sort_by).trim() : '';
name: 'amazon_search_list', const keep_tab_open = data && data.keep_tab_open === true;
desc: amazon_search_list_action.desc, const limit = (() => {
params: amazon_search_list_action.params, const n = data && Object.prototype.hasOwnProperty.call(data, 'limit') ? Number(data.limit) : 100;
handler: amazon_search_list_action.handler, 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', name: 'amazon_set_language',
@@ -26,7 +123,56 @@ export const amazon_actions = [
params: { params: {
lang: { type: 'string', desc: 'EN / ES / AR / DE / HE / KO / PT / ZH_CN(默认) / ZH_TW', default: 'ZH_CN' }, 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', name: 'amazon_product_detail',
@@ -34,14 +180,40 @@ export const amazon_actions = [
params: { params: {
product_url: { type: 'string', desc: '商品详情页完整 URL含 /dp/ASIN', default: 'https://www.amazon.com/-/zh/dp/B0B56CHMSC' }, product_url: { type: 'string', desc: '商品详情页完整 URL含 /dp/ASIN', default: 'https://www.amazon.com/-/zh/dp/B0B56CHMSC' },
}, },
handler: (data, sendResponse) => handler: async (data, sendResponse) => {
run_amazon_pdp_action( const send_action = create_send_action(sendResponse);
data && data.product_url,
injected_amazon_product_detail, const normalized = guard_sync(() => normalize_product_url(data && data.product_url));
[], if (!normalized.ok) {
'amazon_product_detail', send_action('amazon_product_detail', fail_response((normalized.error && normalized.error.message) || String(normalized.error), {
sendResponse, 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', 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' }, product_url: { type: 'string', desc: '商品详情页完整 URL', default: 'https://www.amazon.com/-/zh/dp/B0B56CHMSC' },
limit: { type: 'number', desc: '最多条数(默认 50上限 100', default: 50 }, limit: { type: 'number', desc: '最多条数(默认 50上限 100', default: 50 },
}, },
handler: (data, sendResponse) => handler: async (data, sendResponse) => {
run_amazon_pdp_action( const send_action = create_send_action(sendResponse);
data && data.product_url,
injected_amazon_product_reviews, const normalized = guard_sync(() => normalize_product_url(data && data.product_url));
[{ limit: data && data.limit != null ? Number(data.limit) : 50 }], if (!normalized.ok) {
'amazon_product_reviews', send_action('amazon_product_reviews', fail_response((normalized.error && normalized.error.message) || String(normalized.error), {
sendResponse, 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', name: 'amazon_product_detail_reviews',
@@ -67,8 +266,7 @@ export const amazon_actions = [
limit: { type: 'number', desc: '最多评论条数(默认 50上限 100', default: 50 }, limit: { type: 'number', desc: '最多评论条数(默认 50上限 100', default: 50 },
skip_detail: { type: 'boolean', desc: '当日已拉过详情则跳过详情提取', default: false }, skip_detail: { type: 'boolean', desc: '当日已拉过详情则跳过详情提取', default: false },
}, },
handler: (data, sendResponse) => handler: (data, sendResponse) => run_amazon_pdp_action_multi(
run_amazon_pdp_action_multi(
data && data.product_url, data && data.product_url,
[ [
...(data && data.skip_detail === true ? [] : [{ name: 'detail', injected_fn: injected_amazon_product_detail, inject_args: [] }]), ...(data && data.skip_detail === true ? [] : [{ name: 'detail', injected_fn: injected_amazon_product_detail, inject_args: [] }]),

View File

@@ -54,6 +54,24 @@ export function sleep_ms(ms) {
return new Promise((resolve) => setTimeout(resolve, Number.isFinite(t) ? Math.max(0, t) : 0)); 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) { 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; 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) { for (let i = 0; i < rounds; i += 1) {
@@ -99,26 +117,42 @@ export function injected_amazon_homepage_search(params) {
'input.nav-input[type="submit"]', 'input.nav-input[type="submit"]',
], 2000); ], 2000);
if (btn) { 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'); const form = input.form || input.closest('form');
if (form && typeof form.requestSubmit === 'function') { if (form && typeof form.requestSubmit === 'function') {
try {
form.requestSubmit(); form.requestSubmit();
return { ok: true, method: 'request_submit' }; return { ok: true, method: 'request_submit' };
} catch (_) {
// continue fallback
}
} }
if (form && typeof form.submit === 'function') { if (form && typeof form.submit === 'function') {
try {
form.submit(); form.submit();
return { ok: true, method: 'form_submit' }; return { ok: true, method: 'form_submit' };
} catch (_) {
// continue fallback
}
} }
try { try {
input.focus();
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); 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('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 })); 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' }; return { ok: true, method: 'keyboard_enter' };
} catch (_) { } catch (_) {
// ignore and return explicit error // 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) { 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_ZH_HOME_URL = 'https://www.amazon.com/-/zh/ref=nav_logo';
const AMAZON_HOME_FOR_LANG = const AMAZON_HOME_FOR_LANG =
'https://www.amazon.com/customer-preferences/edit?ie=UTF8&preferencesReturnUrl=%2F&ref_=topnav_lang_ais&language=zh_CN&currency=HKD'; 'https://www.amazon.com/customer-preferences/edit?ie=UTF8&preferencesReturnUrl=%2F&ref_=topnav_lang_ais&language=zh_CN&currency=HKD';
@@ -450,12 +493,7 @@ export async function run_amazon_search_list_action(data, sendResponse) {
bestseller: 'exact-aware-popularity-rank', bestseller: 'exact-aware-popularity-rank',
}; };
const sort_s = Object.prototype.hasOwnProperty.call(sort_map, sort_by) ? sort_map[sort_by] : ''; const sort_s = Object.prototype.hasOwnProperty.call(sort_map, sort_by) ? sort_map[sort_by] : '';
const send_action = (action, payload) => { const send_action = create_send_action(sendResponse);
if (typeof sendResponse === 'function') {
sendResponse({ action, data: payload });
sendResponse.log && sendResponse.log(payload);
}
};
const tab_task = create_tab_task(AMAZON_ZH_HOME_URL) const tab_task = create_tab_task(AMAZON_ZH_HOME_URL)
.set_latest(false) .set_latest(false)
.set_bounds({ top: 20, left: 20, width: 1440, height: 900 }) .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 () => { 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 tab.execute_script(injected_amazon_search_list, [{ category_keyword, sort_by, debug: true }], 'document_idle');
await try_solve_amazon_validate_captcha(tab, 3); 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'); 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; 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) { 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); url = await wait_until_search_list_url(tab.id, 45000);
await tab.wait_complete(); 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 }], [{ url: next_url, category_keyword, sort_by }],
'document_idle', '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 : []; const items = injected_result && Array.isArray(injected_result.items) ? injected_result.items : [];
items.forEach((it) => { items.forEach((it) => {
const k = it && (it.asin || it.url) ? String(it.asin || it.url) : null; 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 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 code = Object.prototype.hasOwnProperty.call(mapping, raw_lang) ? raw_lang : 'ZH_CN';
const send_action = (action, payload) => { const send_action = create_send_action(sendResponse);
if (typeof sendResponse === 'function') {
sendResponse({ action, data: payload });
sendResponse.log && sendResponse.log(payload);
}
};
const tab_task = create_tab_task(AMAZON_HOME_FOR_LANG) const tab_task = create_tab_task(AMAZON_HOME_FOR_LANG)
.set_latest(false) .set_latest(false)
.set_bounds({ top: 20, left: 20, width: 1280, height: 900 }); .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 () => { const payload = await tab.wait_update_complete_once(async () => {
await tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle'); 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 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) { if (!inj || !inj.ok) {
throw new Error((inj && inj.error) || 'switch_language_failed'); 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) { export async function run_amazon_pdp_action(product_url, injected_fn, inject_args, action_name, sendResponse) {
const send_action = (action, payload) => { const send_action = create_send_action(sendResponse);
if (typeof sendResponse === 'function') {
sendResponse({ action, data: payload });
sendResponse.log && sendResponse.log(payload);
}
};
const normalized = guard_sync(() => normalize_product_url(product_url)); const normalized = guard_sync(() => normalize_product_url(product_url));
if (!normalized.ok) { if (!normalized.ok) {
send_action(action_name, fail_response((normalized.error && normalized.error.message) || String(normalized.error), { 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 tab.execute_script(injected_amazon_validate_captcha_continue, [], 'document_idle');
await try_solve_amazon_validate_captcha(tab, 3); await try_solve_amazon_validate_captcha(tab, 3);
const raw_list = await tab.execute_script(injected_fn, inject_args || [], 'document_idle'); 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 }; return { tab_id: tab.id, product_url: url, result };
}).catch((err) => { }).catch((err) => {
send_action(action_name, fail_response((err && err.message) || String(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) { export async function run_amazon_pdp_action_multi(product_url, steps, action_name, sendResponse) {
const send_action = (action, payload) => { const send_action = create_send_action(sendResponse);
if (typeof sendResponse === 'function') {
sendResponse({ action, data: payload });
sendResponse.log && sendResponse.log(payload);
}
};
const normalized = guard_sync(() => normalize_product_url(product_url)); const normalized = guard_sync(() => normalize_product_url(product_url));
if (!normalized.ok) { if (!normalized.ok) {
send_action(action_name, fail_response((normalized.error && normalized.error.message) || String(normalized.error), { 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 || []) { for (const step of steps || []) {
if (!step || !step.name || typeof step.injected_fn !== 'function') continue; 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 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; results[step.name] = result;
} }
return { tab_id: tab.id, product_url: url, result: results }; return { tab_id: tab.id, product_url: url, result: results };

View File

@@ -7,9 +7,9 @@
import { amazon_actions } from './amazon.js'; import { amazon_actions } from './amazon.js';
export { amazon_actions }; export { amazon_actions };
// Amazon 工具函数 // 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'; 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, run_amazon_pdp_action_multi, run_amazon_search_list_action, run_amazon_set_language_action }; 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 = { export const Actions = {

View File

@@ -1,27 +1,18 @@
import { amazon_actions, getAllActionsMeta, getActionByName } from '../actions/index.js'; 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 调用 // action 注册表:供 UI 下拉选择 + server bridge 调用
let action_list = []; let action_list = [];
try { try {
if (Array.isArray(amazon_actions)) { if (Array.isArray(amazon_actions)) {
action_list = amazon_actions.filter(item => item && typeof item === 'object' && item.name); 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 { } else {
debug_log('warn', 'amazon_actions is not an array:', amazon_actions); console.warn('amazon_actions is not an array:', amazon_actions);
} }
} catch (error) { } catch (error) {
debug_log('error', 'Failed to load amazon_actions:', error); console.error('Failed to load amazon_actions:', error);
} }
const list_actions_meta = () => getAllActionsMeta(); 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 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) => { const emit_ui_event = (event_name, payload) => {
try { try {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
@@ -47,7 +43,10 @@ const emit_ui_event = (event_name, payload) => {
ts: Date.now(), ts: Date.now(),
}, (response) => { }, (response) => {
if (chrome.runtime.lastError) { 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) { } catch (error) {
@@ -60,22 +59,22 @@ chrome.browserAction.onClicked.addListener(() => {
}); });
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 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) { if (!message) {
debug_log('warn', 'Empty message received'); console.warn('Empty message received');
return; return;
} }
// UI 自己发出来的事件background 不处理 // UI 自己发出来的事件background 不处理
if (message.channel === 'ui_event') { if (message.channel === 'ui_event') {
debug_log('log', 'Ignoring ui_event message'); console.log('Ignoring ui_event message');
return; return;
} }
// content -> background 的推送消息(通用) // content -> background 的推送消息(通用)
if (message.type === 'push') { if (message.type === 'push') {
debug_log('log', 'Processing push message:', message.action); console.log('Processing push message:', message.action);
emit_ui_event('push', { emit_ui_event('push', {
type: 'push', type: 'push',
action: message.action, action: message.action,
@@ -87,21 +86,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// UI -> background 的 action 调用 // UI -> background 的 action 调用
if (!message.action) { if (!message.action) {
debug_log('error', 'Missing action in message'); console.error('Missing action in message');
sendResponse && sendResponse({ ok: false, error: '缺少 action' }); sendResponse && sendResponse({ ok: false, error: '缺少 action' });
return; return;
} }
// UI 获取 action 元信息(用于下拉/默认参数) // UI 获取 action 元信息(用于下拉/默认参数)
if (message.action === 'meta_actions') { if (message.action === 'meta_actions') {
debug_log('log', 'Returning actions meta'); console.log('Returning actions meta');
sendResponse({ ok: true, data: list_actions_meta() }); sendResponse({ ok: true, data: list_actions_meta() });
return; return;
} }
// UI 刷新后台(重启 background page // UI 刷新后台(重启 background page
if (message.action === 'reload_background') { if (message.action === 'reload_background') {
debug_log('log', 'Reloading background page'); console.log('Reloading background page');
sendResponse({ ok: true }); sendResponse({ ok: true });
setTimeout(() => { setTimeout(() => {
location.reload(); location.reload();
@@ -112,20 +111,20 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const action_item = getActionByName(message.action); const action_item = getActionByName(message.action);
const action_handler = action_item && typeof action_item.handler === 'function' ? action_item.handler : null; const action_handler = action_item && typeof action_item.handler === 'function' ? action_item.handler : null;
if (!action_handler) { if (!action_handler) {
debug_log('error', 'Unknown action:', message.action); console.error('Unknown action:', message.action);
sendResponse({ ok: false, error: '未知 action: ' + message.action }); sendResponse({ ok: false, error: '未知 action: ' + message.action });
return; return;
} }
const request_id = `${Date.now()}_${Math.random().toString().slice(2)}`; 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 }); 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 action_send_response = create_action_send_response(sender);
// 添加超时处理 // 添加超时处理
const timeout = setTimeout(() => { 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', { emit_ui_event('response', {
type: 'response', type: 'response',
request_id, request_id,
@@ -140,14 +139,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
try { try {
const res = await action_handler(message.data || {}, action_send_response); const res = await action_handler(message.data || {}, action_send_response);
clearTimeout(timeout); 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 }); emit_ui_event('response', { type: 'response', request_id, ok: true, data: res, sender });
sendResponse({ ok: true, data: res, request_id }); sendResponse({ ok: true, data: res, request_id });
} catch (err) { } catch (err) {
clearTimeout(timeout); clearTimeout(timeout);
const error = (err && err.message) || String(err); const error = (err && err.message) || String(err);
const stack = (err && err.stack) || ''; 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 }); emit_ui_event('response', { type: 'response', request_id, ok: false, error, stack, sender });
sendResponse({ ok: false, error, stack, request_id }); sendResponse({ ok: false, error, stack, request_id });
} }

View File

@@ -159,6 +159,15 @@ body {
overflow: auto; overflow: auto;
} }
/* 响应区:不显示滚动条,长行自动换行(含无空格长串) */
.pre_response {
overflow: visible;
overflow-x: hidden;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.pre_scroll { .pre_scroll {
max-height: 520px; max-height: 520px;
overflow: auto; overflow: auto;

View File

@@ -20,6 +20,10 @@
<div class="card_title">调用</div> <div class="card_title">调用</div>
<div class="form"> <div class="form">
<div>
<button id="btn_bg_reload" class="btn">刷新后台</button>
</div>
<label class="label">方法名action</label> <label class="label">方法名action</label>
<!-- action 列表:由 background 注册;这里仅提供快速手动调用入口 --> <!-- action 列表:由 background 注册;这里仅提供快速手动调用入口 -->
<select id="action_name" class="input"> <select id="action_name" class="input">
@@ -39,7 +43,7 @@
<div class="row"> <div class="row">
<button id="btn_run" class="btn primary">执行</button> <button id="btn_run" class="btn primary">执行</button>
<button id="btn_clear" class="btn">清空日志</button> <button id="btn_clear" class="btn">清空日志</button>
<button id="btn_bg_reload" class="btn">刷新后台</button>
</div> </div>
<div class="hint"> <div class="hint">
@@ -54,7 +58,7 @@
<label class="label">动作日志</label> <label class="label">动作日志</label>
<pre id="action_log" class="pre pre_small"></pre> <pre id="action_log" class="pre pre_small"></pre>
<div class="card_title">响应</div> <div class="card_title">响应</div>
<pre id="last_response" class="pre"></pre> <pre id="last_response" class="pre pre_response"></pre>
</div> </div>
</div> </div>
</div> </div>