1
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
// Amazon:直接在 handler 中编写逻辑,简化封装层级
|
// 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 { create_tab_task, ok_response, fail_response, guard_sync, response_code, sleep_ms, get_tab_url } from '../libs/index.js';
|
||||||
|
import { 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, try_solve_amazon_validate_captcha, wait_until_search_list_url } 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_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_ZH_HOME_URL = 'https://www.amazon.com/-/zh/ref=nav_logo';
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ export const amazon_actions = [
|
|||||||
category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' },
|
category_keyword: { type: 'string', desc: '类目关键词(在首页搜索框输入后点搜索,进入列表再抓)', default: '野餐包' },
|
||||||
sort_by: { type: 'string', desc: '排序方式:featured / price_asc / price_desc / review / newest / bestseller', default: 'featured' },
|
sort_by: { type: 'string', desc: '排序方式:featured / price_asc / price_desc / review / newest / bestseller', default: 'featured' },
|
||||||
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: true },
|
||||||
},
|
},
|
||||||
handler: async (data, sendResponse) => {
|
handler: async (data, sendResponse) => {
|
||||||
const send_action = create_send_action(sendResponse);
|
const send_action = create_send_action(sendResponse);
|
||||||
@@ -50,17 +52,16 @@ export const amazon_actions = [
|
|||||||
try {
|
try {
|
||||||
const tab = await tab_task.open_async();
|
const tab = await tab_task.open_async();
|
||||||
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');
|
const href = await get_tab_url(tab.id).catch(() => '');
|
||||||
|
const is_captcha = String(href).includes('/errors/validateCaptcha');
|
||||||
|
if(is_captcha) {
|
||||||
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');
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果在首页搜索页面,则执行首页搜索
|
||||||
|
|
||||||
|
const home_ret = await tab.execute_script(injected_amazon_homepage_search, [{ keyword }], 'document_idle');
|
||||||
|
let home_ok = pick_first_script_result(home_ret);
|
||||||
if (!home_ok || !home_ok.ok) {
|
if (!home_ok || !home_ok.ok) {
|
||||||
const current_url = await get_tab_url(tab.id).catch(() => '');
|
const current_url = await get_tab_url(tab.id).catch(() => '');
|
||||||
const detail = home_ok && typeof home_ok === 'object'
|
const detail = home_ok && typeof home_ok === 'object'
|
||||||
@@ -103,7 +104,6 @@ export const amazon_actions = [
|
|||||||
const list_result = { stage: 'list', limit, total: unique_map.size, items: Array.from(unique_map.values()).slice(0, limit) };
|
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 };
|
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));
|
send_action('amazon_search_list', ok_response(payload));
|
||||||
if (!keep_tab_open) tab.remove(0);
|
if (!keep_tab_open) tab.remove(0);
|
||||||
return payload;
|
return payload;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { create_tab_task, ok_response, fail_response, guard_sync, response_code } from '../libs/index.js';
|
import { create_tab_task, ok_response, fail_response, guard_sync, response_code, sleep_ms, get_tab_url } from '../libs/index.js';
|
||||||
|
|
||||||
// Amazon:注入函数 + action 实现(amazon.js 仅保留 action 壳)
|
// Amazon:注入函数 + action 实现(amazon.js 仅保留 action 壳)
|
||||||
//
|
//
|
||||||
@@ -7,18 +7,15 @@ import { create_tab_task, ok_response, fail_response, guard_sync, response_code
|
|||||||
// - 每个 action 打开 tab 后,通过 tab.set_on_complete_inject 绑定 onUpdated(status=complete) 注入钩子
|
// - 每个 action 打开 tab 后,通过 tab.set_on_complete_inject 绑定 onUpdated(status=complete) 注入钩子
|
||||||
|
|
||||||
// ---------- 页面注入(仅依赖页面 DOM) ----------
|
// ---------- 页面注入(仅依赖页面 DOM) ----------
|
||||||
|
// 注意:injected_* 会经 tabs.executeScript 序列化执行,闭包外变量不会进入页面,辅助函数只能写在各 injected_* 函数体内。
|
||||||
const injected_utils = () => window.__mv2_simple_injected || null;
|
|
||||||
|
|
||||||
const dispatch_human_click = (target_el, options) => {
|
|
||||||
const u = injected_utils();
|
|
||||||
if (u && typeof u.dispatch_human_click === 'function') {
|
|
||||||
return u.dispatch_human_click(target_el, options);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function injected_amazon_validate_captcha_continue() {
|
export function injected_amazon_validate_captcha_continue() {
|
||||||
|
const injected_utils = () => window.__mv2_simple_injected || null;
|
||||||
|
const dispatch_human_click = (target_el, options) => {
|
||||||
|
const u = injected_utils();
|
||||||
|
if (u && typeof u.dispatch_human_click === 'function') return u.dispatch_human_click(target_el, options);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
const href = location.href || '';
|
const href = location.href || '';
|
||||||
const is_captcha = href.includes('/errors/validateCaptcha');
|
const is_captcha = href.includes('/errors/validateCaptcha');
|
||||||
if (!is_captcha) return { ok: true, is_captcha: false, clicked: false, href };
|
if (!is_captcha) return { ok: true, is_captcha: false, clicked: false, href };
|
||||||
@@ -49,12 +46,7 @@ export function is_amazon_validate_captcha_url(tab_url) {
|
|||||||
return tab_url.includes('amazon.') && tab_url.includes('/errors/validateCaptcha');
|
return tab_url.includes('amazon.') && tab_url.includes('/errors/validateCaptcha');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sleep_ms(ms) {
|
export function pick_first_script_result(raw_list) {
|
||||||
const t = Number(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;
|
if (!Array.isArray(raw_list) || raw_list.length === 0) return null;
|
||||||
const first = raw_list[0];
|
const first = raw_list[0];
|
||||||
if (first && typeof first === 'object' && Object.prototype.hasOwnProperty.call(first, 'result')) {
|
if (first && typeof first === 'object' && Object.prototype.hasOwnProperty.call(first, 'result')) {
|
||||||
@@ -89,6 +81,12 @@ export async function try_solve_amazon_validate_captcha(tab, max_round) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function injected_amazon_homepage_search(params) {
|
export function injected_amazon_homepage_search(params) {
|
||||||
|
const injected_utils = () => window.__mv2_simple_injected || null;
|
||||||
|
const dispatch_human_click = (target_el, options) => {
|
||||||
|
const u = injected_utils();
|
||||||
|
if (u && typeof u.dispatch_human_click === 'function') return u.dispatch_human_click(target_el, options);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
const keyword = params && params.keyword ? String(params.keyword).trim() : '';
|
const keyword = params && params.keyword ? String(params.keyword).trim() : '';
|
||||||
if (!keyword) return { ok: false, error: 'empty_keyword' };
|
if (!keyword) return { ok: false, error: 'empty_keyword' };
|
||||||
|
|
||||||
@@ -156,6 +154,12 @@ export function injected_amazon_homepage_search(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function injected_amazon_switch_language(params) {
|
export function injected_amazon_switch_language(params) {
|
||||||
|
const injected_utils = () => window.__mv2_simple_injected || null;
|
||||||
|
const dispatch_human_click = (target_el, options) => {
|
||||||
|
const u = injected_utils();
|
||||||
|
if (u && typeof u.dispatch_human_click === 'function') return u.dispatch_human_click(target_el, options);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
const mapping = {
|
const mapping = {
|
||||||
EN: 'en_US',
|
EN: 'en_US',
|
||||||
ES: 'es_US',
|
ES: 'es_US',
|
||||||
@@ -203,6 +207,12 @@ export function injected_amazon_switch_language(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function injected_amazon_search_list(params) {
|
export function injected_amazon_search_list(params) {
|
||||||
|
const injected_utils = () => window.__mv2_simple_injected || null;
|
||||||
|
const dispatch_human_click = (target_el, options) => {
|
||||||
|
const u = injected_utils();
|
||||||
|
if (u && typeof u.dispatch_human_click === 'function') return u.dispatch_human_click(target_el, options);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
params = params && typeof params === 'object' ? params : {};
|
params = params && typeof params === 'object' ? params : {};
|
||||||
const debug = params.debug === true;
|
const debug = params.debug === true;
|
||||||
const u = injected_utils();
|
const u = injected_utils();
|
||||||
@@ -314,7 +324,7 @@ export function injected_amazon_search_list(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function injected_amazon_product_detail() {
|
export function injected_amazon_product_detail() {
|
||||||
const u = injected_utils();
|
const u = window.__mv2_simple_injected || null;
|
||||||
const norm = u && typeof u.norm_space === 'function' ? u.norm_space : (s) => (s || '').replace(/\s+/g, ' ').trim();
|
const norm = u && typeof u.norm_space === 'function' ? u.norm_space : (s) => (s || '').replace(/\s+/g, ' ').trim();
|
||||||
const asin_match = location.pathname.match(/\/(?:dp|gp\/product)\/([A-Z0-9]{10})/i);
|
const asin_match = location.pathname.match(/\/(?:dp|gp\/product)\/([A-Z0-9]{10})/i);
|
||||||
const asin = asin_match ? asin_match[1].toUpperCase() : null;
|
const asin = asin_match ? asin_match[1].toUpperCase() : null;
|
||||||
@@ -461,15 +471,6 @@ 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';
|
||||||
|
|||||||
@@ -7,27 +7,6 @@
|
|||||||
import { amazon_actions } from './amazon.js';
|
import { amazon_actions } from './amazon.js';
|
||||||
export { amazon_actions };
|
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_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 = {
|
|
||||||
// Amazon 动作列表
|
|
||||||
amazon: amazon_actions,
|
|
||||||
|
|
||||||
// Amazon 工具函数
|
|
||||||
amazonTools: {
|
|
||||||
validateCaptcha: injected_amazon_validate_captcha_continue,
|
|
||||||
isCaptchaUrl: is_amazon_validate_captcha_url,
|
|
||||||
productDetail: injected_amazon_product_detail,
|
|
||||||
productReviews: injected_amazon_product_reviews,
|
|
||||||
runPdpAction: run_amazon_pdp_action,
|
|
||||||
runPdpActionMulti: run_amazon_pdp_action_multi,
|
|
||||||
runSearchListAction: run_amazon_search_list_action,
|
|
||||||
runSetLanguageAction: run_amazon_set_language_action
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取所有动作的元信息
|
// 获取所有动作的元信息
|
||||||
export function getAllActionsMeta() {
|
export function getAllActionsMeta() {
|
||||||
@@ -52,5 +31,8 @@ export function getActionByName(name) {
|
|||||||
return amazon_actions.find(item => item && item.name === name);
|
return amazon_actions.find(item => item && item.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认导出
|
|
||||||
export default Actions;
|
export default {
|
||||||
|
amazon: amazon_actions,
|
||||||
|
};;
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 内部:page world 执行结果回传
|
||||||
|
if (message.channel === 'page_exec_bridge') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// content -> background 的推送消息(通用)
|
// content -> background 的推送消息(通用)
|
||||||
if (message.type === 'push') {
|
if (message.type === 'push') {
|
||||||
console.log('Processing push message:', message.action);
|
console.log('Processing push message:', message.action);
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ export { raw_execute_script, inject_file, ensure_injected, execute_script, open_
|
|||||||
import { bind_action_meta } from './action_meta.js';
|
import { bind_action_meta } from './action_meta.js';
|
||||||
export { bind_action_meta };
|
export { bind_action_meta };
|
||||||
|
|
||||||
|
// 通用异步工具
|
||||||
|
export function sleep_ms(ms) {
|
||||||
|
const t = Number(ms);
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, Number.isFinite(t) ? Math.max(0, t) : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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) : '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 便捷的统一导出对象(可选使用)
|
// 便捷的统一导出对象(可选使用)
|
||||||
export const Libs = {
|
export const Libs = {
|
||||||
// 响应处理
|
// 响应处理
|
||||||
|
|||||||
@@ -63,29 +63,112 @@ export async function raw_execute_script(tab_id, fn, args = [], run_at = 'docume
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const code = build_code(fn, args);
|
const request_id = `${Date.now()}_${Math.random().toString().slice(2)}`;
|
||||||
|
const event_name = `__mv2_simple_page_exec_done__${request_id}`;
|
||||||
|
|
||||||
|
// 页面上下文执行:用事件把结果从 page world 回传到 extension world
|
||||||
|
const page_exec_stmt = (() => {
|
||||||
|
if (typeof fn === 'function') {
|
||||||
|
// build_code(fn, args) 返回一段“可执行并产生值”的代码(末尾可能带分号)
|
||||||
|
return `__exec_result = ${build_code(fn, args)}`;
|
||||||
|
}
|
||||||
|
// fn 为 string:包进 IIFE,允许用户在字符串里使用 return 返回值
|
||||||
|
return `__exec_result = (function () { ${fn} })();`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const page_script_text = `
|
||||||
|
(function () {
|
||||||
|
const __request_id = ${JSON.stringify(request_id)};
|
||||||
|
const __event_name = ${JSON.stringify(event_name)};
|
||||||
|
let __exec_result;
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
${page_exec_stmt}
|
||||||
|
return __exec_result;
|
||||||
|
})
|
||||||
|
.then((__result) => {
|
||||||
|
window.dispatchEvent(new CustomEvent(__event_name, {
|
||||||
|
detail: { request_id: __request_id, ok: true, result: __result }
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((__err) => {
|
||||||
|
const __e = __err;
|
||||||
|
window.dispatchEvent(new CustomEvent(__event_name, {
|
||||||
|
detail: {
|
||||||
|
request_id: __request_id,
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: (__e && __e.message) ? __e.message : String(__e),
|
||||||
|
stack: (__e && __e.stack) ? __e.stack : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const bootstrap_code = `
|
||||||
|
(function () {
|
||||||
|
const __request_id = ${JSON.stringify(request_id)};
|
||||||
|
const __event_name = ${JSON.stringify(event_name)};
|
||||||
|
const __on_done = (ev) => {
|
||||||
|
const detail = ev && ev.detail ? ev.detail : null;
|
||||||
|
if (!detail || detail.request_id !== __request_id) return;
|
||||||
|
window.removeEventListener(__event_name, __on_done, true);
|
||||||
|
try {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
channel: 'page_exec_bridge',
|
||||||
|
request_id: __request_id,
|
||||||
|
ok: !!detail.ok,
|
||||||
|
result: detail.result,
|
||||||
|
error_message: detail.error && detail.error.message ? detail.error.message : null,
|
||||||
|
error_stack: detail.error && detail.error.stack ? detail.error.stack : null
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener(__event_name, __on_done, true);
|
||||||
|
const el = document.createElement('script');
|
||||||
|
el.type = 'text/javascript';
|
||||||
|
el.textContent = ${JSON.stringify(page_script_text)};
|
||||||
|
(document.head || document.documentElement).appendChild(el);
|
||||||
|
el.parentNode && el.parentNode.removeChild(el);
|
||||||
|
})();
|
||||||
|
`.trim();
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
|
chrome.runtime.onMessage.removeListener(on_message);
|
||||||
reject(new Error(`Script execution timeout for tab ${tab_id}`));
|
reject(new Error(`Script execution timeout for tab ${tab_id}`));
|
||||||
}, 30000); // 30秒超时
|
}, 30000); // 30秒超时
|
||||||
|
|
||||||
|
const on_message = (message) => {
|
||||||
|
if (!message || message.channel !== 'page_exec_bridge' || message.request_id !== request_id) return;
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
chrome.runtime.onMessage.removeListener(on_message);
|
||||||
|
if (message.ok) return resolve([message.result]);
|
||||||
|
const err = new Error(message.error_message || 'page script execution failed');
|
||||||
|
err.stack = message.error_stack || err.stack;
|
||||||
|
return reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(on_message);
|
||||||
|
|
||||||
chrome.tabs.executeScript(
|
chrome.tabs.executeScript(
|
||||||
tab_id,
|
tab_id,
|
||||||
{
|
{
|
||||||
code,
|
code: bootstrap_code,
|
||||||
runAt: run_at,
|
runAt: run_at,
|
||||||
},
|
},
|
||||||
(result) => {
|
() => {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
chrome.runtime.onMessage.removeListener(on_message);
|
||||||
const error = new Error(chrome.runtime.lastError.message);
|
const error = new Error(chrome.runtime.lastError.message);
|
||||||
error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : 'EXECUTION_ERROR';
|
error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : 'EXECUTION_ERROR';
|
||||||
return reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(result || []);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -164,16 +247,6 @@ export async function inject_file(tab_id, file, run_at = 'document_idle') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从执行结果中提取第一个帧的值
|
|
||||||
* @param {Array} raw_list - Chrome 执行脚本返回的结果数组
|
|
||||||
* @returns {*} 第一个帧的值,如果数组为空则返回 null
|
|
||||||
*/
|
|
||||||
const pick_first_frame_value = (raw_list) => {
|
|
||||||
if (!Array.isArray(raw_list) || raw_list.length === 0) return null;
|
|
||||||
return raw_list[0]?.result ?? raw_list[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确保注入脚本已加载到指定标签页
|
* 确保注入脚本已加载到指定标签页
|
||||||
* @param {number} tab_id - 标签页ID
|
* @param {number} tab_id - 标签页ID
|
||||||
@@ -187,9 +260,8 @@ export async function ensure_injected(tab_id, maxRetries = 3) {
|
|||||||
|
|
||||||
// 检查是否已经注入
|
// 检查是否已经注入
|
||||||
try {
|
try {
|
||||||
const injected = pick_first_frame_value(
|
const injected_frames = await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle');
|
||||||
await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle')
|
const injected = Array.isArray(injected_frames) && injected_frames.length ? (injected_frames[0]?.result ?? injected_frames[0]) : null;
|
||||||
);
|
|
||||||
if (injected === true) return true;
|
if (injected === true) return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果检查失败,可能是标签页不存在,继续尝试注入
|
// 如果检查失败,可能是标签页不存在,继续尝试注入
|
||||||
@@ -204,9 +276,8 @@ export async function ensure_injected(tab_id, maxRetries = 3) {
|
|||||||
await inject_file(tab_id, 'injected/injected.js', 'document_idle');
|
await inject_file(tab_id, 'injected/injected.js', 'document_idle');
|
||||||
|
|
||||||
// 验证注入是否成功
|
// 验证注入是否成功
|
||||||
const injected = pick_first_frame_value(
|
const injected_frames = await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle');
|
||||||
await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle')
|
const injected = Array.isArray(injected_frames) && injected_frames.length ? (injected_frames[0]?.result ?? injected_frames[0]) : null;
|
||||||
);
|
|
||||||
|
|
||||||
if (injected === true) return true;
|
if (injected === true) return true;
|
||||||
|
|
||||||
@@ -351,28 +422,24 @@ const attach_tab_helpers = (tab) => {
|
|||||||
|
|
||||||
let running = false;
|
let running = false;
|
||||||
const once = !!(options && options.once === true);
|
const once = !!(options && options.once === true);
|
||||||
const on_error = options && typeof options.on_error === 'function' ? options.on_error : null;
|
|
||||||
const listener = async (updated_tab_id, change_info, updated_tab) => {
|
const listener = async (updated_tab_id, change_info, updated_tab) => {
|
||||||
if (updated_tab_id !== tab.id) return;
|
if (updated_tab_id !== tab.id) return;
|
||||||
if (!change_info || change_info.status !== 'complete') return;
|
if (!change_info || change_info.status !== 'complete') return;
|
||||||
if (running) return;
|
if (running) return;
|
||||||
running = true;
|
running = true;
|
||||||
const tab_obj = attach_tab_helpers(updated_tab || tab);
|
const tab_obj = attach_tab_helpers(updated_tab || tab);
|
||||||
try {
|
// try {
|
||||||
|
debugger
|
||||||
await fn(tab_obj, change_info);
|
await fn(tab_obj, change_info);
|
||||||
if (once) {
|
if (once) {
|
||||||
tab.off_update_complete && tab.off_update_complete();
|
tab.off_update_complete && tab.off_update_complete();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
if (on_error) {
|
|
||||||
on_error(err, tab_obj, change_info);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('[tab_on_update] fail', { tab_id: tab.id, error: (err && err.message) || String(err) });
|
// console.warn('[tab_on_update] fail', { tab_id: tab.id, error: (err && err.message) || String(err) });
|
||||||
}
|
// } finally {
|
||||||
} finally {
|
|
||||||
running = false;
|
running = false;
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener(listener);
|
chrome.tabs.onUpdated.addListener(listener);
|
||||||
@@ -656,6 +723,7 @@ export function create_tab_task(url) {
|
|||||||
|
|
||||||
const tab_done = await new Promise((resolve) => {
|
const tab_done = await new Promise((resolve) => {
|
||||||
const on_updated = (tab_id, change_info, tab) => {
|
const on_updated = (tab_id, change_info, tab) => {
|
||||||
|
|
||||||
if (tab_id !== tab0.id) return;
|
if (tab_id !== tab0.id) return;
|
||||||
if (change_info.status !== 'complete') return;
|
if (change_info.status !== 'complete') return;
|
||||||
chrome.tabs.onUpdated.removeListener(on_updated);
|
chrome.tabs.onUpdated.removeListener(on_updated);
|
||||||
|
|||||||
@@ -83,6 +83,22 @@ body {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label_row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label_row input {
|
||||||
|
margin-top: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.input,
|
.input,
|
||||||
.textarea {
|
.textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
<div id="action_params_desc" class="hint" style="margin-top:6px; white-space:pre-wrap;"></div>
|
<div id="action_params_desc" class="hint" style="margin-top:6px; white-space:pre-wrap;"></div>
|
||||||
<textarea id="action_params" class="textarea" spellcheck="false">{}</textarea>
|
<textarea id="action_params" class="textarea" spellcheck="false">{}</textarea>
|
||||||
|
|
||||||
|
<label class="label_row"><input type="checkbox" id="opt_keep_tab_open" checked /> 执行后保留自动化窗口(keep_tab_open,覆盖下方 JSON 与本 action 默认值)</label>
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const btn_clear_el = document.getElementById('btn_clear');
|
|||||||
const btn_bg_reload_el = document.getElementById('btn_bg_reload');
|
const btn_bg_reload_el = document.getElementById('btn_bg_reload');
|
||||||
const last_response_el = document.getElementById('last_response');
|
const last_response_el = document.getElementById('last_response');
|
||||||
const action_log_el = document.getElementById('action_log');
|
const action_log_el = document.getElementById('action_log');
|
||||||
|
const opt_keep_tab_open_el = document.getElementById('opt_keep_tab_open');
|
||||||
let actions_meta = {};
|
let actions_meta = {};
|
||||||
const ui_state = { last_result: null, actions: [] };
|
const ui_state = { last_result: null, actions: [] };
|
||||||
|
|
||||||
@@ -66,9 +67,14 @@ const push_action = (obj) => {
|
|||||||
render_state();
|
render_state();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const apply_keep_tab_open_override = (parsed) => {
|
||||||
|
if (!opt_keep_tab_open_el || !parsed || typeof parsed !== 'object' || parsed.__parse_error) return parsed;
|
||||||
|
return { ...parsed, keep_tab_open: opt_keep_tab_open_el.checked === true };
|
||||||
|
};
|
||||||
|
|
||||||
btn_run_el.addEventListener('click', () => {
|
btn_run_el.addEventListener('click', () => {
|
||||||
const action = action_name_el.value;
|
const action = action_name_el.value;
|
||||||
const params = safe_json_parse(action_params_el.value || '{}');
|
const params = apply_keep_tab_open_override(safe_json_parse(action_params_el.value || '{}'));
|
||||||
|
|
||||||
push_action({ type: 'call', action, params });
|
push_action({ type: 'call', action, params });
|
||||||
ui_state.last_result = { running: true, action, params };
|
ui_state.last_result = { running: true, action, params };
|
||||||
|
|||||||
Reference in New Issue
Block a user