1
This commit is contained in:
@@ -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 = {
|
// 公共的 send_action 函数
|
||||||
desc: 'Amazon 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取',
|
const create_send_action = (sendResponse) => (actionName, response) => {
|
||||||
params: {
|
sendResponse({ action: actionName, ...response });
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const amazon_actions = [
|
export const amazon_actions = [
|
||||||
{
|
{
|
||||||
name: 'amazon_search_list',
|
name: 'amazon_search_list',
|
||||||
desc: amazon_search_list_action.desc,
|
desc: 'Amazon 搜索列表:先打开中文首页,搜索框输入类目并搜索,再分页抓取',
|
||||||
params: amazon_search_list_action.params,
|
params: {
|
||||||
handler: amazon_search_list_action.handler,
|
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',
|
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,15 +266,14 @@ 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: [] }]),
|
{ name: 'reviews', injected_fn: injected_amazon_product_reviews, inject_args: [{ limit: data && data.limit != null ? Number(data.limit) : 50 }] },
|
||||||
{ name: 'reviews', injected_fn: injected_amazon_product_reviews, inject_args: [{ limit: data && data.limit != null ? Number(data.limit) : 50 }] },
|
],
|
||||||
],
|
'amazon_product_detail_reviews',
|
||||||
'amazon_product_detail_reviews',
|
sendResponse,
|
||||||
sendResponse,
|
),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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') {
|
||||||
form.requestSubmit();
|
try {
|
||||||
return { ok: true, method: 'request_submit' };
|
form.requestSubmit();
|
||||||
|
return { ok: true, method: 'request_submit' };
|
||||||
|
} catch (_) {
|
||||||
|
// continue fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (form && typeof form.submit === 'function') {
|
if (form && typeof form.submit === 'function') {
|
||||||
form.submit();
|
try {
|
||||||
return { ok: true, method: 'form_submit' };
|
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¤cy=HKD';
|
'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',
|
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);
|
||||||
const home_ret = await tab.execute_script(injected_amazon_homepage_search, [{ keyword }], 'document_idle');
|
let home_ok = null;
|
||||||
const home_ok = Array.isArray(home_ret) ? home_ret[0] : home_ret;
|
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) {
|
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 };
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user