diff --git a/mv2_simple_crx/src/actions/amazon.js b/mv2_simple_crx/src/actions/amazon.js index 579996d..d6f0ce5 100644 --- a/mv2_simple_crx/src/actions/amazon.js +++ b/mv2_simple_crx/src/actions/amazon.js @@ -1,6 +1,8 @@ // 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_ZH_HOME_URL = 'https://www.amazon.com/-/zh/ref=nav_logo'; @@ -17,11 +19,11 @@ export const amazon_actions = [ 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 }, + keep_tab_open: { type: 'boolean', desc: '调试用:不自动关闭窗口,方便手动刷新观察轨迹', default: true }, }, 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; @@ -40,27 +42,26 @@ export const amazon_actions = [ 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); + 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); } + + // 如果在首页搜索页面,则执行首页搜索 + + 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) { const current_url = await get_tab_url(tab.id).catch(() => ''); 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) }; 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; @@ -125,7 +125,7 @@ export const amazon_actions = [ }, handler: async (data, sendResponse) => { const send_action = create_send_action(sendResponse); - + const mapping = { EN: 'en_US', ES: 'es_US', @@ -139,11 +139,11 @@ export const amazon_actions = [ }; 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 () => { @@ -161,7 +161,7 @@ export const amazon_actions = [ }); return { tab_id: tab.id, lang: inj.lang, url: final_url }; }); - + send_action('amazon_set_language', ok_response(payload)); tab.remove(0); return payload; @@ -182,7 +182,7 @@ export const amazon_actions = [ }, 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), { @@ -190,10 +190,10 @@ export const amazon_actions = [ })); 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 () => { @@ -202,7 +202,7 @@ export const amazon_actions = [ 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; @@ -224,7 +224,7 @@ export const amazon_actions = [ }, 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), { @@ -232,10 +232,10 @@ export const amazon_actions = [ })); 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 () => { @@ -245,7 +245,7 @@ export const amazon_actions = [ 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; diff --git a/mv2_simple_crx/src/actions/amazon_tool.js b/mv2_simple_crx/src/actions/amazon_tool.js index 4cbaed9..e153c63 100644 --- a/mv2_simple_crx/src/actions/amazon_tool.js +++ b/mv2_simple_crx/src/actions/amazon_tool.js @@ -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 壳) // @@ -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) 注入钩子 // ---------- 页面注入(仅依赖页面 DOM) ---------- - -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; -}; +// 注意:injected_* 会经 tabs.executeScript 序列化执行,闭包外变量不会进入页面,辅助函数只能写在各 injected_* 函数体内。 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 is_captcha = href.includes('/errors/validateCaptcha'); 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'); } -export function sleep_ms(ms) { - 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) { +export 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')) { @@ -89,6 +81,12 @@ export async function try_solve_amazon_validate_captcha(tab, max_round) { } 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() : ''; 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) { + 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 = { EN: 'en_US', ES: 'es_US', @@ -203,6 +207,12 @@ export function injected_amazon_switch_language(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 : {}; const debug = params.debug === true; const u = injected_utils(); @@ -314,7 +324,7 @@ export function injected_amazon_search_list(params) { } 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 asin_match = location.pathname.match(/\/(?:dp|gp\/product)\/([A-Z0-9]{10})/i); 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_HOME_FOR_LANG = 'https://www.amazon.com/customer-preferences/edit?ie=UTF8&preferencesReturnUrl=%2F&ref_=topnav_lang_ais&language=zh_CN¤cy=HKD'; diff --git a/mv2_simple_crx/src/actions/index.js b/mv2_simple_crx/src/actions/index.js index d36f84c..05faf19 100644 --- a/mv2_simple_crx/src/actions/index.js +++ b/mv2_simple_crx/src/actions/index.js @@ -7,27 +7,6 @@ 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_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() { @@ -52,5 +31,8 @@ export function getActionByName(name) { return amazon_actions.find(item => item && item.name === name); } -// 默认导出 -export default Actions; + +export default { + amazon: amazon_actions, +};; + diff --git a/mv2_simple_crx/src/background/index.js b/mv2_simple_crx/src/background/index.js index c182a36..ce21882 100644 --- a/mv2_simple_crx/src/background/index.js +++ b/mv2_simple_crx/src/background/index.js @@ -72,6 +72,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return; } + // 内部:page world 执行结果回传 + if (message.channel === 'page_exec_bridge') { + return; + } + // content -> background 的推送消息(通用) if (message.type === 'push') { console.log('Processing push message:', message.action); diff --git a/mv2_simple_crx/src/libs/index.js b/mv2_simple_crx/src/libs/index.js index d099081..f57279b 100644 --- a/mv2_simple_crx/src/libs/index.js +++ b/mv2_simple_crx/src/libs/index.js @@ -15,6 +15,21 @@ export { raw_execute_script, inject_file, ensure_injected, execute_script, open_ import { bind_action_meta } from './action_meta.js'; 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 = { // 响应处理 diff --git a/mv2_simple_crx/src/libs/tabs.js b/mv2_simple_crx/src/libs/tabs.js index 37f97c7..7574692 100644 --- a/mv2_simple_crx/src/libs/tabs.js +++ b/mv2_simple_crx/src/libs/tabs.js @@ -27,11 +27,11 @@ const build_code = (fn, args) => { } return `(${funcStr})();`; } - + if (typeof fn === 'string') { return fn; } - + throw new TypeError('fn must be a function or string'); }; @@ -48,44 +48,127 @@ export async function raw_execute_script(tab_id, fn, args = [], run_at = 'docume if (!Number.isInteger(tab_id) || tab_id <= 0) { throw new Error('Invalid tab_id: must be a positive integer'); } - + if (!fn || (typeof fn !== 'function' && typeof fn !== 'string')) { throw new Error('Invalid fn: must be a function or string'); } - + if (!Array.isArray(args)) { throw new Error('Invalid args: must be an array'); } - + const validRunAt = ['document_start', 'document_end', 'document_idle']; if (!validRunAt.includes(run_at)) { throw new Error(`Invalid run_at: must be one of ${validRunAt.join(', ')}`); } 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) => { const timeoutId = setTimeout(() => { + chrome.runtime.onMessage.removeListener(on_message); reject(new Error(`Script execution timeout for tab ${tab_id}`)); }, 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( tab_id, { - code, + code: bootstrap_code, runAt: run_at, }, - (result) => { - clearTimeout(timeoutId); - + () => { if (chrome.runtime.lastError) { + clearTimeout(timeoutId); + chrome.runtime.onMessage.removeListener(on_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'; - return reject(error); + reject(error); } - - resolve(result || []); } ); }); @@ -111,16 +194,16 @@ export async function inject_file(tab_id, file, run_at = 'document_idle') { if (!Number.isInteger(tab_id) || tab_id <= 0) { throw new Error('Invalid tab_id: must be a positive integer'); } - + if (!file || typeof file !== 'string') { throw new Error('Invalid file: must be a non-empty string'); } - + // 验证文件路径格式 if (!file.match(/^[\w\-./]+$/)) { throw new Error('Invalid file path: contains invalid characters'); } - + const validRunAt = ['document_start', 'document_end', 'document_idle']; if (!validRunAt.includes(run_at)) { throw new Error(`Invalid run_at: must be one of ${validRunAt.join(', ')}`); @@ -140,15 +223,15 @@ export async function inject_file(tab_id, file, run_at = 'document_idle') { }, (result) => { clearTimeout(timeoutId); - + if (chrome.runtime.lastError) { const error = new Error(chrome.runtime.lastError.message); - error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : - chrome.runtime.lastError.message?.includes('Cannot access') ? 'FILE_NOT_FOUND' : 'INJECTION_ERROR'; + error.code = chrome.runtime.lastError.message?.includes('No tab with id') ? 'TAB_NOT_FOUND' : + chrome.runtime.lastError.message?.includes('Cannot access') ? 'FILE_NOT_FOUND' : 'INJECTION_ERROR'; error.file = file; return reject(error); } - + resolve(true); } ); @@ -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 @@ -187,9 +260,8 @@ export async function ensure_injected(tab_id, maxRetries = 3) { // 检查是否已经注入 try { - const injected = pick_first_frame_value( - await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle') - ); + const injected_frames = 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; } catch (error) { // 如果检查失败,可能是标签页不存在,继续尝试注入 @@ -202,14 +274,13 @@ export async function ensure_injected(tab_id, maxRetries = 3) { try { // 约定:扩展根目录=src,因此 file 使用 src 内相对路径 await inject_file(tab_id, 'injected/injected.js', 'document_idle'); - + // 验证注入是否成功 - const injected = pick_first_frame_value( - await raw_execute_script(tab_id, () => !!window.__mv2_simple_injected, [], 'document_idle') - ); - + const injected_frames = 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 (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, 500 * attempt)); @@ -351,28 +422,24 @@ const attach_tab_helpers = (tab) => { let running = false; 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) => { if (updated_tab_id !== tab.id) return; if (!change_info || change_info.status !== 'complete') return; if (running) return; running = true; const tab_obj = attach_tab_helpers(updated_tab || tab); - try { - await fn(tab_obj, change_info); - if (once) { - tab.off_update_complete && tab.off_update_complete(); - } - } catch (err) { - if (on_error) { - on_error(err, tab_obj, change_info); - } else { - // eslint-disable-next-line no-console - console.warn('[tab_on_update] fail', { tab_id: tab.id, error: (err && err.message) || String(err) }); - } - } finally { - running = false; + // try { + debugger + await fn(tab_obj, change_info); + if (once) { + tab.off_update_complete && tab.off_update_complete(); } + // } catch (err) { + // eslint-disable-next-line no-console + // console.warn('[tab_on_update] fail', { tab_id: tab.id, error: (err && err.message) || String(err) }); + // } finally { + running = false; + // } }; chrome.tabs.onUpdated.addListener(listener); @@ -471,7 +538,7 @@ export async function open_tab(url, options = {}) { clearTimeout(timeoutId); chrome.tabs.onUpdated.removeListener(on_updated); - + try { const enhancedTab = attach_tab_helpers(updated_tab); resolve({ tab_id, tab: enhancedTab }); @@ -560,7 +627,7 @@ export async function close_tab(tab_id, delayOrOptions = {}) { try { return await new Promise((resolve, reject) => { const delay = Math.max(0, opts.delay); - + if (delay === 0) { // 立即关闭 chrome.tabs.remove(tab_id, () => { @@ -656,6 +723,7 @@ export function create_tab_task(url) { const tab_done = await new Promise((resolve) => { const on_updated = (tab_id, change_info, tab) => { + if (tab_id !== tab0.id) return; if (change_info.status !== 'complete') return; chrome.tabs.onUpdated.removeListener(on_updated); diff --git a/mv2_simple_crx/src/ui/index.css b/mv2_simple_crx/src/ui/index.css index 4f6b796..9c81f0a 100644 --- a/mv2_simple_crx/src/ui/index.css +++ b/mv2_simple_crx/src/ui/index.css @@ -83,6 +83,22 @@ body { 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, .textarea { width: 100%; diff --git a/mv2_simple_crx/src/ui/index.html b/mv2_simple_crx/src/ui/index.html index 42e67cd..9b38a0f 100644 --- a/mv2_simple_crx/src/ui/index.html +++ b/mv2_simple_crx/src/ui/index.html @@ -40,6 +40,8 @@
+ +
diff --git a/mv2_simple_crx/src/ui/index.js b/mv2_simple_crx/src/ui/index.js index 1d60f54..c09860a 100644 --- a/mv2_simple_crx/src/ui/index.js +++ b/mv2_simple_crx/src/ui/index.js @@ -6,6 +6,7 @@ const btn_clear_el = document.getElementById('btn_clear'); const btn_bg_reload_el = document.getElementById('btn_bg_reload'); const last_response_el = document.getElementById('last_response'); const action_log_el = document.getElementById('action_log'); +const opt_keep_tab_open_el = document.getElementById('opt_keep_tab_open'); let actions_meta = {}; const ui_state = { last_result: null, actions: [] }; @@ -66,9 +67,14 @@ const push_action = (obj) => { 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', () => { 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 }); ui_state.last_result = { running: true, action, params };