1
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
// 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 = {
|
||||
// 公共的 send_action 函数
|
||||
const create_send_action = (sendResponse) => (actionName, response) => {
|
||||
sendResponse({ action: actionName, ...response });
|
||||
};
|
||||
|
||||
export const amazon_actions = [
|
||||
{
|
||||
name: 'amazon_search_list',
|
||||
desc: 'Amazon 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取',
|
||||
params: {
|
||||
category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' },
|
||||
@@ -10,15 +19,103 @@ const amazon_search_list_action = {
|
||||
limit: { type: 'number', desc: '抓取数量上限(默认 100,最大 200)', default: 100 },
|
||||
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 = [
|
||||
{
|
||||
name: 'amazon_search_list',
|
||||
desc: amazon_search_list_action.desc,
|
||||
params: amazon_search_list_action.params,
|
||||
handler: amazon_search_list_action.handler,
|
||||
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,8 +266,7 @@ 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(
|
||||
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: [] }]),
|
||||
|
||||
@@ -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') {
|
||||
try {
|
||||
form.requestSubmit();
|
||||
return { ok: true, method: 'request_submit' };
|
||||
} catch (_) {
|
||||
// continue fallback
|
||||
}
|
||||
}
|
||||
if (form && typeof form.submit === 'function') {
|
||||
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);
|
||||
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_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) {
|
||||
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 };
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
<div class="card_title">调用</div>
|
||||
|
||||
<div class="form">
|
||||
<div>
|
||||
<button id="btn_bg_reload" class="btn">刷新后台</button>
|
||||
</div>
|
||||
|
||||
<label class="label">方法名(action)</label>
|
||||
<!-- action 列表:由 background 注册;这里仅提供快速手动调用入口 -->
|
||||
<select id="action_name" class="input">
|
||||
@@ -39,7 +43,7 @@
|
||||
<div class="row">
|
||||
<button id="btn_run" class="btn primary">执行</button>
|
||||
<button id="btn_clear" class="btn">清空日志</button>
|
||||
<button id="btn_bg_reload" class="btn">刷新后台</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
@@ -54,7 +58,7 @@
|
||||
<label class="label">动作日志</label>
|
||||
<pre id="action_log" class="pre pre_small"></pre>
|
||||
<div class="card_title">响应</div>
|
||||
<pre id="last_response" class="pre"></pre>
|
||||
<pre id="last_response" class="pre pre_response"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user