@@ -0,0 +1,479 @@
// Amazon: 注入函数 + action 实现( amazon.js 仅保留 action 壳)
//
// 约定:
// - injected_* 在页面上下文执行,只依赖 DOM
// - 每个 action 打开 tab 后,通过 tab.set_on_complete_inject 绑定 onUpdated(status=complete) 注入钩子
// ---------- 页面注入(仅依赖页面 DOM) ----------
function dispatch _human _click ( target _el , options ) {
const el = target _el ;
if ( ! el ) return false ;
options = options && typeof options === 'object' ? options : { } ;
const pointer _id = Number . isFinite ( options . pointer _id ) ? options . pointer _id : 1 ;
const pointer _type = options . pointer _type ? String ( options . pointer _type ) : 'mouse' ;
try { el . scrollIntoView ( { block : 'center' , inline : 'center' } ) ; } catch ( _ ) { }
try { el . focus && el . focus ( ) ; } catch ( _ ) { }
const rect = el . getBoundingClientRect ( ) ;
const ox = Number . isFinite ( options . offset _x ) ? options . offset _x : 0 ;
const oy = Number . isFinite ( options . offset _y ) ? options . offset _y : 0 ;
const x = Math . max ( 1 , Math . floor ( rect . left + rect . width / 2 + ox ) ) ;
const y = Math . max ( 1 , Math . floor ( rect . top + rect . height / 2 + oy ) ) ;
const base = { bubbles : true , cancelable : true , view : window , clientX : x , clientY : y } ;
try {
if ( typeof PointerEvent === 'function' ) {
el . dispatchEvent ( new PointerEvent ( 'pointerover' , { ... base , pointerId : pointer _id , pointerType : pointer _type , isPrimary : true } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointerenter' , { ... base , pointerId : pointer _id , pointerType : pointer _type , isPrimary : true } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointermove' , { ... base , pointerId : pointer _id , pointerType : pointer _type , isPrimary : true } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointerdown' , { ... base , pointerId : pointer _id , pointerType : pointer _type , isPrimary : true , buttons : 1 } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointerup' , { ... base , pointerId : pointer _id , pointerType : pointer _type , isPrimary : true , buttons : 0 } ) ) ;
}
} catch ( _ ) { }
el . dispatchEvent ( new MouseEvent ( 'mousemove' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'mouseover' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'mousedown' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'mouseup' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'click' , base ) ) ;
return true ;
}
export function injected _amazon _validate _captcha _continue ( ) {
const href = location . href || '' ;
const is _captcha = href . includes ( '/errors/validateCaptcha' ) ;
if ( ! is _captcha ) return { ok : true , is _captcha : false , clicked : false , href } ;
const btn =
document . querySelector ( 'form[action="/errors/validateCaptcha"] button[type="submit"].a-button-text' ) ||
document . querySelector ( 'form[action*="validateCaptcha"] input[type="submit"]' ) ||
document . querySelector ( 'form[action*="validateCaptcha"] button[type="submit"]' ) ||
document . querySelector ( 'input[type="submit"][value*="Continue"]' ) ||
document . querySelector ( 'button[type="submit"]' ) ;
const clicked = btn ? dispatch _human _click ( btn ) : false ;
if ( ! clicked ) {
const form = document . querySelector ( 'form[action*="validateCaptcha"]' ) ;
if ( form ) {
try {
form . submit ( ) ;
return { ok : true , is _captcha : true , clicked : true , method : 'submit' , href } ;
} catch ( _ ) { }
}
}
return { ok : true , is _captcha : true , clicked , method : clicked ? 'dispatch' : 'none' , href } ;
}
export function is _amazon _validate _captcha _url ( tab _url ) {
if ( ! tab _url || typeof tab _url !== 'string' ) return false ;
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 ) ) ;
}
export async function try _solve _amazon _validate _captcha ( tab , max _round ) {
const rounds = Number . isFinite ( max _round ) ? Math . max ( 1 , Math . min ( 5 , Math . floor ( max _round ) ) ) : 2 ;
for ( let i = 0 ; i < rounds ; i += 1 ) {
const tab _state = await new Promise ( ( resolve ) => {
chrome . tabs . get ( tab . id , ( t ) => resolve ( t || null ) ) ;
} ) ;
const url = tab _state && tab _state . url ? String ( tab _state . url ) : '' ;
if ( ! is _amazon _validate _captcha _url ( url ) ) return true ;
await tab . execute _script ( injected _amazon _validate _captcha _continue , [ ] , 'document_idle' ) ;
await sleep _ms ( 800 + Math . floor ( Math . random ( ) * 600 ) ) ;
await tab . wait _complete ( ) ;
await sleep _ms ( 300 ) ;
}
return false ;
}
export function injected _amazon _homepage _search ( params ) {
const keyword = params && params . keyword ? String ( params . keyword ) . trim ( ) : '' ;
if ( ! keyword ) return { ok : false , error : 'empty_keyword' } ;
const input =
document . querySelector ( '#twotabsearchtextbox' ) ||
document . querySelector ( 'input#nav-search-keywords' ) ||
document . querySelector ( 'input[name="field-keywords"]' ) ;
if ( ! input ) return { ok : false , error : 'no_search_input' } ;
input . focus ( ) ;
input . value = keyword ;
input . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) ) ;
input . dispatchEvent ( new Event ( 'change' , { bubbles : true } ) ) ;
const btn =
document . querySelector ( '#nav-search-submit-button' ) ||
document . querySelector ( '#nav-search-bar-form input[type="submit"]' ) ||
document . querySelector ( 'form[role="search"] input[type="submit"]' ) ;
if ( btn ) {
return { ok : dispatch _human _click ( btn ) } ;
}
const form = input . closest ( 'form' ) ;
if ( form ) {
form . submit ( ) ;
return { ok : true } ;
}
return { ok : false , error : 'no_submit' } ;
}
export function injected _amazon _switch _language ( params ) {
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 = params && params . lang != null ? String ( params . lang ) . trim ( ) . toUpperCase ( ) : 'ZH_CN' ;
const code = Object . prototype . hasOwnProperty . call ( mapping , raw ) ? raw : 'ZH_CN' ;
const switch _lang = mapping [ code ] ;
const href _sel = ` a[href="#switch-lang= ${ switch _lang } "] ` ;
const deadline = Date . now ( ) + 6000 ;
let link = null ;
while ( Date . now ( ) < deadline ) {
link = document . querySelector ( href _sel ) ;
if ( link ) {
const r = link . getBoundingClientRect ( ) ;
if ( r . width > 0 && r . height > 0 ) break ;
}
const t0 = performance . now ( ) ;
while ( performance . now ( ) - t0 < 40 ) { }
}
if ( ! link ) return { ok : false , error : 'lang_option_timeout' , lang : code } ;
dispatch _human _click ( link ) ;
const save _deadline = Date . now ( ) + 6000 ;
let save = null ;
while ( Date . now ( ) < save _deadline ) {
save =
document . querySelector ( 'input[type="submit"][value*="Save"]' ) ||
document . querySelector ( 'input[type="submit"][aria-labelledby*="icp-save-button"]' ) ||
document . querySelector ( 'span.icp-save-button input[type="submit"]' ) ;
if ( save ) break ;
const t1 = performance . now ( ) ;
while ( performance . now ( ) - t1 < 40 ) { }
}
if ( save ) {
dispatch _human _click ( save ) ;
}
return { ok : true , lang : code } ;
}
export function injected _amazon _search _list ( params ) {
params = params && typeof params === 'object' ? params : { } ;
const debug = params . debug === true ;
// validateCaptcha: 在 onUpdated(complete) 钩子里也能自动处理
if ( ( location . href || '' ) . includes ( '/errors/validateCaptcha' ) ) {
function dispatch _human _click _local ( el ) {
if ( ! el ) return false ;
try { el . scrollIntoView ( { block : 'center' , inline : 'center' } ) ; } catch ( _ ) { }
try { el . focus && el . focus ( ) ; } catch ( _ ) { }
const rect = el . getBoundingClientRect ( ) ;
const x = Math . max ( 1 , Math . floor ( rect . left + rect . width / 2 ) ) ;
const y = Math . max ( 1 , Math . floor ( rect . top + rect . height / 2 ) ) ;
const base = { bubbles : true , cancelable : true , view : window , clientX : x , clientY : y } ;
try {
if ( typeof PointerEvent === 'function' ) {
el . dispatchEvent ( new PointerEvent ( 'pointerover' , { ... base , pointerId : 1 , pointerType : 'mouse' , isPrimary : true } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointerenter' , { ... base , pointerId : 1 , pointerType : 'mouse' , isPrimary : true } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointermove' , { ... base , pointerId : 1 , pointerType : 'mouse' , isPrimary : true } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointerdown' , { ... base , pointerId : 1 , pointerType : 'mouse' , isPrimary : true , buttons : 1 } ) ) ;
el . dispatchEvent ( new PointerEvent ( 'pointerup' , { ... base , pointerId : 1 , pointerType : 'mouse' , isPrimary : true , buttons : 0 } ) ) ;
}
} catch ( _ ) { }
el . dispatchEvent ( new MouseEvent ( 'mousemove' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'mouseover' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'mousedown' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'mouseup' , base ) ) ;
el . dispatchEvent ( new MouseEvent ( 'click' , base ) ) ;
return true ;
}
const btn =
document . querySelector ( 'form[action="/errors/validateCaptcha"] button[type="submit"].a-button-text' ) ||
document . querySelector ( 'form[action*="validateCaptcha"] input[type="submit"]' ) ||
document . querySelector ( 'form[action*="validateCaptcha"] button[type="submit"]' ) ||
document . querySelector ( 'input[type="submit"][value*="Continue"]' ) ||
document . querySelector ( 'button[type="submit"]' ) ;
const clicked = btn ? dispatch _human _click _local ( btn ) : false ;
if ( debug ) {
// eslint-disable-next-line no-console
console . log ( '[amazon][on_complete] validateCaptcha' , { clicked , href : location . href } ) ;
}
return { stage : 'captcha' , href : location . href , clicked } ;
}
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 ;
const s = String ( text ) . replace ( /[(),]/g , ' ' ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
const m = s . match ( /(\d+(?:\.\d+)?)/ ) ;
return m ? Number ( m [ 1 ] ) : null ;
}
function pick _int ( text ) {
if ( ! text ) return null ;
const raw = String ( text ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
const u = raw . toUpperCase ( ) . replace ( /,/g , '' ) ;
const km = u . match ( /([\d.]+)\s*K\b/ ) ;
if ( km ) return Math . round ( parseFloat ( km [ 1 ] ) * 1000 ) ;
const mm = u . match ( /([\d.]+)\s*M\b/ ) ;
if ( mm ) return Math . round ( parseFloat ( mm [ 1 ] ) * 1000000 ) ;
const digits = raw . replace ( /[^\d]/g , '' ) ;
return digits ? Number ( digits ) : null ;
}
function abs _url ( href ) {
try {
return new URL ( href , location . origin ) . toString ( ) ;
} catch ( _ ) {
return href ;
}
}
function parse _asin _from _url ( url ) {
if ( ! url || typeof url !== 'string' ) return null ;
const m = url . match ( /\/dp\/([A-Z0-9]{10})/i ) || url . match ( /\/gp\/product\/([A-Z0-9]{10})/i ) ;
return m ? m [ 1 ] . toUpperCase ( ) : null ;
}
function extract _results ( ) {
const items = [ ] ;
const nodes = document . querySelectorAll ( 'div.s-main-slot div[data-component-type="s-search-result"]' ) ;
nodes . forEach ( ( el , idx ) => {
const asin = ( el . getAttribute ( 'data-asin' ) || '' ) . trim ( ) || null ;
const title _el = el . querySelector ( 'h2 span' ) || el . querySelector ( 'h2' ) ;
const title = title _el ? title _el . textContent . trim ( ) : null ;
const a = el . querySelector ( 'a[href*="/dp/"], a[href*="/gp/product/"]' ) ;
const href = a ? a . getAttribute ( 'href' ) : null ;
const item _url = href ? abs _url ( href ) : null ;
const price _el = el . querySelector ( 'span.a-price > span.a-offscreen' ) ;
const price = price _el ? price _el . textContent . trim ( ) : null ;
const reviews _block = el . querySelector ( 'div[data-cy="reviews-block"]' ) || el ;
const rating _text = ( ( ) => {
const t1 = reviews _block . querySelector ( 'span.a-icon-alt' ) ;
if ( t1 && t1 . textContent ) return t1 . textContent . trim ( ) ;
const t2 = reviews _block . querySelector ( 'span.a-size-small.a-color-base[aria-hidden="true"]' ) ;
if ( t2 && t2 . textContent ) return t2 . textContent . trim ( ) ;
return null ;
} ) ( ) ;
const rating = ( ( ) => {
const n = pick _number ( rating _text ) ;
return Number . isFinite ( n ) ? n : null ;
} ) ( ) ;
const review _count _text = ( ( ) => {
const a1 = reviews _block . querySelector ( 'a[href*="#customerReviews"]' ) ;
if ( a1 && a1 . textContent ) return a1 . textContent . trim ( ) ;
const a2 = reviews _block . querySelector (
'a[aria-label*="rating"], a[aria-label*="ratings"], a[aria-label*="评级"], a[aria-label*="评价"]' ,
) ;
if ( a2 && a2 . getAttribute ( 'aria-label' ) ) return a2 . getAttribute ( 'aria-label' ) . trim ( ) ;
const s1 = reviews _block . querySelector ( 'span.a-size-mini.puis-normal-weight-text' ) ;
if ( s1 && s1 . textContent ) return s1 . textContent . trim ( ) ;
return null ;
} ) ( ) ;
const review _count = ( ( ) => {
const n = pick _int ( review _count _text ) ;
return Number . isFinite ( n ) ? n : null ;
} ) ( ) ;
items . push ( {
index : idx + 1 ,
asin : asin || parse _asin _from _url ( item _url ) ,
title ,
url : item _url ,
price ,
rating ,
rating _text ,
review _count ,
review _count _text ,
} ) ;
} ) ;
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 out = { start _url , href : location . href , category _keyword , sort _by , total : items . length , items , next _url : pick _next _url ( ) } ;
if ( debug ) {
// eslint-disable-next-line no-console
console . log ( '[amazon][on_complete] search_list' , {
href : out . href ,
total : out . total ,
has _next : ! ! out . next _url ,
} ) ;
try {
window . _ _amazon _debug _last _search _list = out ;
} catch ( _ ) { }
}
return out ;
}
export function injected _amazon _product _detail ( ) {
const norm = ( 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 ;
const product _info = { } ;
function set _info ( k , v , max _len ) {
k = norm ( k ) ;
v = norm ( v ) ;
max _len = max _len || 600 ;
if ( ! k || ! v || k . length > 100 ) return ;
if ( v . length > max _len ) v = v . slice ( 0 , max _len ) ;
if ( ! product _info [ k ] || v . length > product _info [ k ] . length ) product _info [ k ] = v ;
}
const table _roots =
'#productOverview_feature_div tr, #poExpander table tr, #productDetails_detailBullets_sections1 tr, ' +
'#productDetails_techSpec_section_1 tr, table.prodDetTable tr, #productFactsDesktopExpander tr, ' +
'#technicalSpecifications_feature_div table tr, #productDetails_db_sections tr' ;
document . querySelectorAll ( table _roots ) . forEach ( ( tr ) => {
const tds = tr . querySelectorAll ( 'td' ) ;
const th = tr . querySelector ( 'th' ) ;
const td = tr . querySelector ( 'td' ) ;
if ( tds . length >= 2 ) set _info ( tds [ 0 ] . innerText , tds [ 1 ] . innerText ) ;
else if ( th && td && th !== td ) set _info ( th . innerText , td . innerText ) ;
} ) ;
const detail _extra _lines = [ ] ;
document . querySelectorAll ( '#detailBullets_feature_div li, #rpi-attribute-values_feature_div li' ) . forEach ( ( li ) => {
const t = li . innerText . replace ( /\u200f|\u200e/g , ' ' ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
const m = t . match ( /^(.{1,80}?)\s*[: :]\s*(.+)$/ ) ;
if ( m ) set _info ( m [ 1 ] , m [ 2 ] , 1200 ) ;
else if ( t . length > 8 && t . length < 800 ) detail _extra _lines . push ( t ) ;
} ) ;
const title _el = document . querySelector ( '#productTitle' ) ;
const title = title _el ? norm ( title _el . textContent ) : null ;
const price _el =
document . querySelector ( '#corePrice_feature_div .a-price .a-offscreen' ) ||
document . querySelector ( '#tp_price_block_total_price_ww .a-offscreen' ) ||
document . querySelector ( '#price .a-offscreen' ) ||
document . querySelector ( '.reinventPricePriceToPayMargin .a-offscreen' ) ||
document . querySelector ( '.a-price .a-offscreen' ) ;
const price = price _el ? price _el . textContent . trim ( ) : null ;
const brand _el = document . querySelector ( '#bylineInfo' ) ;
const brand _line = brand _el ? norm ( brand _el . textContent ) : null ;
const brand _store _url = document . querySelector ( '#bylineInfo a[href]' ) ? . href || null ;
const rating _stars = document . querySelector ( '#acrPopover' ) ? . getAttribute ( 'title' ) ||
document . querySelector ( '#averageCustomerReviews .a-icon-alt' ) ? . textContent ? . trim ( ) || null ;
const review _count _text = document . querySelector ( '#acrCustomerReviewText' ) ? . textContent ? . trim ( ) || null ;
const ac _badge = norm ( document . querySelector ( '#acBadge_feature_div' ) ? . innerText ) || null ;
const social _proof = norm ( document . querySelector ( '#socialProofingAsinFaceout_feature_div' ) ? . innerText ) || null ;
const bestseller _hint = norm ( document . querySelector ( '#zeitgeistBadge_feature_div' ) ? . innerText ) ? . slice ( 0 , 200 ) || null ;
const bullets = [ ] ;
document . querySelectorAll ( '#feature-bullets ul li span.a-list-item' ) . forEach ( ( el ) => {
const t = norm ( el . textContent ) ;
if ( t ) bullets . push ( t ) ;
} ) ;
let delivery _hint = null ;
const del = document . querySelector ( '#deliveryBlockMessage, #mir-layout-DELIVERY_BLOCK-slot-PRIMARY_DELIVERY_MESSAGE_LARGE' ) ;
if ( del ) delivery _hint = norm ( del . innerText ) . slice ( 0 , 500 ) ;
return {
stage : 'detail' ,
asin ,
title ,
price ,
brand _line ,
brand _store _url ,
rating _stars ,
review _count _text ,
ac _badge ,
social _proof ,
bestseller _hint ,
product _info ,
detail _extra _lines ,
bullets ,
delivery _hint ,
url : location . href ,
} ;
}
export function injected _amazon _product _reviews ( params ) {
const raw = params && params . limit != null ? Number ( params . limit ) : 50 ;
const limit = Number . isFinite ( raw ) ? Math . max ( 1 , Math . min ( 100 , Math . floor ( raw ) ) ) : 50 ;
const nodes = document . querySelectorAll ( '[data-hook="review"]' ) ;
const items = [ ] ;
nodes . forEach ( ( r ) => {
if ( items . length >= limit ) return ;
const author _el = r . querySelector ( '.a-profile-name' ) ;
const author = author _el ? author _el . textContent . trim ( ) : null ;
const title _el = r . querySelector ( '[data-hook="review-title"]' ) ;
const title = title _el ? title _el . innerText . replace ( /\s+/g , ' ' ) . trim ( ) : null ;
const body _el = r . querySelector ( '[data-hook="review-body"]' ) ;
const body = body _el ? body _el . innerText . replace ( /\s+/g , ' ' ) . trim ( ) : null ;
const rating _el = r . querySelector ( '[data-hook="review-star-rating"]' ) ;
const rating _text = rating _el ? rating _el . textContent . trim ( ) : null ;
const date _el = r . querySelector ( '[data-hook="review-date"]' ) ;
const date = date _el ? date _el . textContent . trim ( ) : null ;
const cr = r . querySelector ( '[id^="customer_review-"]' ) ;
const review _id = r . id || ( cr && cr . id ? cr . id . replace ( 'customer_review-' , '' ) : null ) ;
items . push ( { index : items . length + 1 , review _id , author , rating _text , title , date , body } ) ;
} ) ;
return { stage : 'reviews' , limit , total : items . length , items , url : location . href } ;
}
export function normalize _product _url ( u ) {
let s = u ? String ( u ) . trim ( ) : '' ;
if ( ! s ) throw new Error ( '缺少 product_url' ) ;
if ( s . startsWith ( '//' ) ) s = 'https:' + s ;
if ( ! /^https?:\/\//i . test ( s ) ) s = 'https://' + s ;
const url _obj = new URL ( s ) ;
if ( ! url _obj . hostname . includes ( 'amazon.' ) ) throw new Error ( 'product_url 需为亚马逊域名' ) ;
if ( ! /\/dp\/[A-Z0-9]{10}/i . test ( url _obj . pathname ) && ! /\/gp\/product\/[A-Z0-9]{10}/i . test ( url _obj . pathname ) ) {
throw new Error ( 'product_url 需包含 /dp/ASIN 或 /gp/product/ASIN' ) ;
}
return url _obj . toString ( ) ;
}
export function is _amazon _search _list _url ( tab _url ) {
if ( ! tab _url || typeof tab _url !== 'string' ) return false ;
if ( ! tab _url . includes ( 'amazon.com' ) ) return false ;
if ( ! /\/s(\?|\/)/ . test ( tab _url ) ) return false ;
return tab _url . includes ( 'k=' ) || tab _url . includes ( 'keywords=' ) || tab _url . includes ( 'field-keywords' ) ;
}
export function wait _until _search _list _url ( tab _id , timeout _ms ) {
const deadline = Date . now ( ) + ( timeout _ms || 45000 ) ;
return new Promise ( ( resolve , reject ) => {
const tick = ( ) => {
chrome . tabs . get ( tab _id , ( tab ) => {
if ( chrome . runtime . lastError ) return reject ( new Error ( chrome . runtime . lastError . message ) ) ;
const u = tab && tab . url ? tab . url : '' ;
if ( is _amazon _search _list _url ( u ) ) return resolve ( u ) ;
if ( Date . now ( ) >= deadline ) return reject ( new Error ( '等待首页搜索跳转到列表页超时' ) ) ;
setTimeout ( tick , 400 ) ;
} ) ;
} ;
tick ( ) ;
} ) ;
}