From bc7c2c81bafeb5e8bd3ad7ffdba492e8e53d97be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 18 Mar 2026 09:45:00 +0800 Subject: [PATCH] 1 --- mv2_simple_crx/src/actions/amazon.js | 96 ++++++++++++++++--- .../src/injected/amazon_search_list.js | 15 +++ mv2_simple_crx/src/ui/index.html | 1 + mv2_simple_crx/src/ui/index.js | 15 +++ 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/mv2_simple_crx/src/actions/amazon.js b/mv2_simple_crx/src/actions/amazon.js index 0b33af6..f3baaa1 100644 --- a/mv2_simple_crx/src/actions/amazon.js +++ b/mv2_simple_crx/src/actions/amazon.js @@ -6,21 +6,38 @@ import { injected_amazon_search_list } from '../injected/amazon_search_list.js'; export function amazon_search_list(data, sendResponse) { return new Promise(async (resolve, reject) => { 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 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 || '野餐包'; - // 用你给的 URL 作为模板,只替换 k 参数,其它参数保持一致 + 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] : ''; + + // 内置 URL,只替换 k / s 参数,其它参数保持一致 const default_url = (() => { const u = new URL('https://www.amazon.com/s'); u.searchParams.set('k', keyword); u.searchParams.set('__mk_zh_CN', '亚马逊网站'); u.searchParams.set('crid', 'ZKNCI4U8BBAP'); - u.searchParams.set('sprefix', '野餐bao,caps,388'); u.searchParams.set('ref', 'nb_sb_noss'); + if (sort_s) u.searchParams.set('s', sort_s); + else u.searchParams.delete('s'); return u.toString(); })(); - const url = (data && data.url) ? String(data.url).trim() : default_url; + const url = default_url; let times = 0; @@ -39,17 +56,67 @@ export function amazon_search_list(data, sendResponse) { try { const tab = await tab_task.open_async(); - const injected_result_list = await tab.execute_script(injected_amazon_search_list, [{ url, category_keyword }], 'document_idle'); - const injected_result = Array.isArray(injected_result_list) ? injected_result_list[0] : null; + const wait_tab_complete = (tab_id) => new Promise((resolve_wait, reject_wait) => { + const on_updated = (updated_tab_id, change_info, updated_tab) => { + if (updated_tab_id !== tab_id) return; + if (change_info.status !== 'complete') return; + chrome.tabs.onUpdated.removeListener(on_updated); + resolve_wait(updated_tab); + }; + chrome.tabs.onUpdated.addListener(on_updated); + setTimeout(() => { + chrome.tabs.onUpdated.removeListener(on_updated); + reject_wait(new Error('等待页面加载超时')); + }, 45000); + }); + + const unique_map = new Map(); + let next_url = url; + let page = 1; + + while (next_url && unique_map.size < limit) { + await new Promise((resolve_nav, reject_nav) => { + chrome.tabs.update(tab.id, { url: next_url, active: true }, () => { + if (chrome.runtime.lastError) return reject_nav(new Error(chrome.runtime.lastError.message)); + resolve_nav(true); + }); + }); + await wait_tab_complete(tab.id); + + const injected_result_list = await tab.execute_script( + injected_amazon_search_list, + [{ url: next_url, category_keyword, sort_by }], + 'document_idle', + ); + const injected_result = Array.isArray(injected_result_list) ? injected_result_list[0] : null; + 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); + }); + + next_url = injected_result && injected_result.next_url ? String(injected_result.next_url) : null; + page += 1; + if (page > 10) break; // 防止死循环(默认 100 条一般 <= 3 页) + } + + const injected_result = { + stage: 'list', + limit, + total: unique_map.size, + items: Array.from(unique_map.values()).slice(0, limit), + }; const result = { code: 0, status: true, message: 'ok', - data: { tab_id: tab.id, url, category_keyword, result: injected_result }, + data: { tab_id: tab.id, url, category_keyword, sort_by: sort_by || 'featured', limit, result: injected_result }, }; send_action('amazon_search_list', result); - resolve({ tab_id: tab.id, url, category_keyword, result: injected_result }); + resolve({ tab_id: tab.id, url, category_keyword, sort_by: sort_by || 'featured', limit, result: injected_result }); // 成功后关闭打开的 tab(同时会关闭 popup window) tab.remove(0); @@ -69,16 +136,21 @@ export function amazon_search_list(data, sendResponse) { amazon_search_list.desc = 'Amazon 搜索结果列表抓取(DOM 解析)'; amazon_search_list.params = { - url: { - type: 'string', - desc: '可选,传入完整搜索 URL(不传则按 category_keyword 生成)', - default: '', - }, 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, + }, }; diff --git a/mv2_simple_crx/src/injected/amazon_search_list.js b/mv2_simple_crx/src/injected/amazon_search_list.js index d33c9ff..a4fcf55 100644 --- a/mv2_simple_crx/src/injected/amazon_search_list.js +++ b/mv2_simple_crx/src/injected/amazon_search_list.js @@ -3,6 +3,7 @@ export function injected_amazon_search_list(params) { const start_url = params && params.url ? String(params.url) : location.href; const category_keyword = params && params.category_keyword ? String(params.category_keyword).trim() : ''; + const sort_by = params && params.sort_by ? String(params.sort_by).trim() : ''; function pick_number(text) { if (!text) return null; @@ -93,14 +94,28 @@ export function injected_amazon_search_list(params) { return items; } + function pick_next_url() { + const a = document.querySelector('a.s-pagination-next'); + if (!a) return null; + const aria_disabled = (a.getAttribute('aria-disabled') || '').trim().toLowerCase(); + if (aria_disabled === 'true') return null; + if (a.classList && a.classList.contains('s-pagination-disabled')) return null; + const href = a.getAttribute('href'); + if (!href) return null; + return abs_url(href); + } + const items = extract_results(); + const next_url = pick_next_url(); // 只返回一次结果,避免 send 太多影响判定 return { start_url, href: location.href, category_keyword, + sort_by, total: items.length, items, + next_url, }; } diff --git a/mv2_simple_crx/src/ui/index.html b/mv2_simple_crx/src/ui/index.html index 9530670..2269a65 100644 --- a/mv2_simple_crx/src/ui/index.html +++ b/mv2_simple_crx/src/ui/index.html @@ -28,6 +28,7 @@ +
diff --git a/mv2_simple_crx/src/ui/index.js b/mv2_simple_crx/src/ui/index.js index e23adb6..c6f2089 100644 --- a/mv2_simple_crx/src/ui/index.js +++ b/mv2_simple_crx/src/ui/index.js @@ -1,4 +1,5 @@ const action_name_el = document.getElementById('action_name'); +const action_params_desc_el = document.getElementById('action_params_desc'); const action_params_el = document.getElementById('action_params'); const btn_run_el = document.getElementById('btn_run'); const btn_clear_el = document.getElementById('btn_clear'); @@ -111,6 +112,20 @@ function refresh_params_editor() { if (!meta) return; const defaults = build_default_params(meta.params); action_params_el.value = JSON.stringify(defaults, null, 2); + + if (action_params_desc_el) { + const schema = meta.params && typeof meta.params === 'object' ? meta.params : {}; + const lines = []; + Object.keys(schema).forEach((k) => { + const item = schema[k] || {}; + const t = item.type ? String(item.type) : 'any'; + const d = item.desc ? String(item.desc) : ''; + const defv = Object.prototype.hasOwnProperty.call(item, 'default') ? item.default : undefined; + const def_text = defv === undefined ? '' : `,默认:${JSON.stringify(defv)}`; + lines.push(`- ${k} (${t})${d ? ':' + d : ''}${def_text}`); + }); + action_params_desc_el.textContent = lines.length ? lines.join('\n') : '(无参数说明)'; + } } function refresh_action_select() {