@ -192,7 +192,22 @@ export class BrowserSession implements BrowserLike {
return this . transport . send < T > ( method , params ) ;
}
private lastFollowCheck = 0 ;
/** 运行时跟随前台标签页:点按钮/搜索常开新标签页,元素操作应落在用户眼前看到的页面(节流 400ms) */
private async followForegroundTab ( ) : Promise < void > {
if ( this . win || ! this . httpBase ) return ;
const now = Date . now ( ) ;
if ( now - this . lastFollowCheck < 400 ) return ;
this . lastFollowCheck = now ;
const pages = await listPageTargets ( this . httpBase ) . catch ( ( ) = > [ ] ) ;
const page = pages [ 0 ] ;
if ( ! page || page . id === this . activeTargetId ) return ;
await this . activateTab ( page . id ) ;
}
private async evaluate < T = unknown > ( expression : string ) : Promise < T > {
await this . followForegroundTab ( ) . catch ( ( ) = > { } ) ;
const result = await this . cdp < { result ? : { value? : T } ; exceptionDetails ? : { exception ? : { description? : string } ; text? : string } } > (
"Runtime.evaluate" ,
{ expression , returnByValue : true , awaitPromise : true }
@ -297,8 +312,18 @@ export class BrowserSession implements BrowserLike {
const localPath = ( start , root ) = > {
const stable = ( item ) = > {
if ( item . id ) {
// 长数字结尾的唯一 id 多为动态记录 id( 如淘宝 item_id_1087512368188) , 泛化为前缀匹配以命中所有同类卡片
const dynamic = /^(.*?[-_])[0-9]{6,}$/ . exec ( item . id ) ;
if ( dynamic ) {
const generalized = '[id^="' + cssEscape ( dynamic [ 1 ] ) + '"]' ;
try { if ( root . querySelectorAll ( generalized ) . length >= 1 ) return generalized ; } catch { }
}
const candidate = '#' + cssEscape ( item . id ) ;
try { if ( root . querySelectorAll ( candidate ) . length === 1 ) return candidate ; } catch { }
if ( dynamic ) {
const generalized = '[id^="' + cssEscape ( dynamic [ 1 ] ) + '"]' ;
try { if ( root . querySelectorAll ( generalized ) . length >= 1 ) return generalized ; } catch { }
}
}
const testId = item . getAttribute ( 'data-testid' ) ;
if ( testId ) {
@ -1153,7 +1178,8 @@ export class BrowserSession implements BrowserLike {
}
async pickElement ( timeoutMs = 90000 ) : Promise < import ( " .. / .. / shared / types " ) .ElementPickResult > {
// 拾取脚本经 Runtime.evaluate 注入并等待用户点击,内置/外部浏览器行为一致
// 拾取脚本注入当前文档,并经 Page.addScriptToEvaluateOnNewDocument 随页面跳转自动重注入;
// 主进程轮询 window.__rpaPickResult——用户在拾取期间可随意翻页/搜索/过滑块,停在哪就能拾哪。
const url = await this . currentUrl ( ) ;
if ( ! url || url === "about:blank" ) {
throw new RuntimeError ( "no_page" , "浏览器还没有打开网页" , { recommendation : "先运行或配置「打开网页」,再拾取元素" } ) ;
@ -1162,241 +1188,63 @@ export class BrowserSession implements BrowserLike {
this . win . show ( ) ;
this . win . focus ( ) ;
}
const result = await this . evaluate < ElementPickResult > ( ` (() => new Promise((resolve, reject) => {
const timeout = $ { JSON . stringify ( timeoutMs ) } ;
if ( window . __rpaElementPicker ? . cancel ) window . __rpaElementPicker . cancel ( ) ;
let host = null ;
let root = null ;
let box = null ;
let badge = null ;
let target = null ;
let locked = false ;
let chain = [ ] ;
let chainIndex = 0 ;
let selected = null ;
let timer = 0 ;
let finished = false ;
const cssEscape = ( value ) = > {
if ( globalThis . CSS && typeof CSS . escape === 'function' ) return CSS . escape ( String ( value ) ) ;
return String ( value ) . replace ( /[^a-zA-Z0-9_-]/g , ( char ) = > '\\\\' + char ) ;
} ;
const isUnique = ( candidate ) = > {
try { return document . querySelectorAll ( candidate ) . length === 1 ; } catch { return false ; }
} ;
const cssPath = ( node ) = > {
const localPath = ( start , root ) = > {
const stable = ( item ) = > {
if ( item . id ) {
const candidate = '#' + cssEscape ( item . id ) ;
try { if ( root . querySelectorAll ( candidate ) . length === 1 ) return candidate ; } catch { }
}
const testId = item . getAttribute ( 'data-testid' ) ;
if ( testId ) {
const candidate = '[data-testid="' + cssEscape ( testId ) + '"]' ;
try { if ( root . querySelectorAll ( candidate ) . length === 1 ) return candidate ; } catch { }
}
return null ;
} ;
const parts = [ ] ;
let current = start ;
while ( current && current . nodeType === 1 ) {
const stableCandidate = stable ( current ) ;
if ( stableCandidate ) {
parts . unshift ( stableCandidate ) ;
return parts . join ( ' > ' ) ;
}
let part = current . tagName . toLowerCase ( ) ;
const parent = current . parentElement ;
if ( parent ) {
const siblings = [ . . . parent . children ] . filter ( ( item ) = > item . tagName === current . tagName ) ;
if ( siblings . length > 1 ) part += ':nth-of-type(' + ( siblings . indexOf ( current ) + 1 ) + ')' ;
}
parts . unshift ( part ) ;
current = parent ;
}
return parts . join ( ' > ' ) ;
} ;
const trees = [ ] ;
let current = node ;
while ( current && current . nodeType === 1 ) {
const root = current . getRootNode ( ) ;
trees . unshift ( localPath ( current , root ) ) ;
current = root instanceof ShadowRoot ? root.host : null ;
const inject = ( ) = >
this . evaluate (
` (() => {
try { window . __rpaElementPicker ? . cancelHard ? . ( ) ; window . __rpaElementPicker ? . cancel ? . ( ) ; } catch { }
try { window . __rpaPickerActive = undefined ; } catch { }
try { document . getElementById ( '__rpa_pick_host' ) ? . remove ( ) ; } catch { }
try { delete window . __rpaPickResult ; $ { PICKER_BOOTSTRAP_SOURCE } } catch { }
} ) ( ) `
) ;
const injectionIds : string [ ] = [ ] ;
const registerInjection = async ( ) = > {
const registered = await this . cdp < { identifier? : string } > ( "Page.addScriptToEvaluateOnNewDocument" , {
source : PICKER_BOOTSTRAP_SOURCE
} ) . catch ( ( ) = > null ) ;
if ( registered ? . identifier ) injectionIds . push ( registered . identifier ) ;
} ;
// 跟随前台标签页:搜索/点按钮常开新标签页,附着目标切过去并重新注入,拾取不中断
const followForegroundTab = async ( ) = > {
if ( this . win || ! this . httpBase ) return ;
const pages = await listPageTargets ( this . httpBase ) . catch ( ( ) = > [ ] ) ;
const page = pages [ 0 ] ;
if ( ! page || page . id === this . activeTargetId ) return ;
await this . activateTab ( page . id ) ;
await this . cdp ( "Page.enable" ) . catch ( ( ) = > { } ) ;
await registerInjection ( ) ;
await inject ( ) ;
} ;
await this . cdp ( "Page.enable" ) . catch ( ( ) = > { } ) ;
await registerInjection ( ) ;
try {
await inject ( ) ;
const deadline = Date . now ( ) + Math . max ( 5000 , timeoutMs ) ;
while ( Date . now ( ) < deadline ) {
await new Promise ( ( resolve ) = > setTimeout ( resolve , 400 ) ) ;
await followForegroundTab ( ) . catch ( ( ) = > { } ) ;
const state = await this . evaluate < { result : { ok : boolean ; value? : ElementPickResult ; reason? : string } | null ; active : boolean } > (
"({ result: window.__rpaPickResult ?? null, active: window.__rpaPickerActive?.active === true })"
) . catch ( ( ) = > null ) ;
if ( ! state ) continue ; // 页面跳转中:等待新文档自动注入
if ( state . result ) {
if ( state . result . ok && state . result . value ) return state . result . value ;
throw new RuntimeError ( "cancelled" , state . result . reason || "已取消元素拾取" ) ;
}
return trees . join ( ' >>> ' ) ;
} ;
const describe = ( node ) = > {
const rect = node . getBoundingClientRect ( ) ;
return {
selector : 'css:' + cssPath ( node ) ,
tag : node.tagName.toLowerCase ( ) ,
id : node.id || undefined ,
className : node.getAttribute ( 'class' ) || undefined ,
name : node.getAttribute ( 'name' ) || undefined ,
type : node . getAttribute ( 'type' ) || undefined ,
placeholder : node.getAttribute ( 'placeholder' ) || undefined ,
label : node.getAttribute ( 'aria-label' ) || node . getAttribute ( 'placeholder' ) || node . getAttribute ( 'name' ) || ( node . innerText || '' ) . trim ( ) . slice ( 0 , 100 ) || undefined ,
text : ( node . innerText || node . textContent || '' ) . trim ( ) . slice ( 0 , 200 ) || undefined ,
value : String ( node . value ? ? '' ) || undefined ,
url : location.href
} ;
} ;
const nodeName = ( node ) = > node . getAttribute ( 'aria-label' ) || node . getAttribute ( 'placeholder' ) || node . getAttribute ( 'name' ) || node . id || ( node . innerText || '' ) . trim ( ) . slice ( 0 , 60 ) || '' ;
const updateBox = ( ) = > {
const node = locked ? selected : target ;
if ( ! node || ! box || ! badge ) return ;
const rect = node . getBoundingClientRect ( ) ;
box . style . left = Math . max ( 0 , rect . left - 3 ) + 'px' ;
box . style . top = Math . max ( 0 , rect . top - 3 ) + 'px' ;
box . style . width = Math . max ( 4 , rect . width + 6 ) + 'px' ;
box . style . height = Math . max ( 4 , rect . height + 6 ) + 'px' ;
const tag = node . tagName . toLowerCase ( ) ;
const name = nodeName ( node ) ;
if ( locked ) {
badge . textContent = '已锁定 ' + ( chainIndex + 1 ) + '/' + chain . length + ' · ' + tag + ( name ? ' · ' + name . slice ( 0 , 42 ) : '' ) + ' · [PageUp/PageDown]调层级 [空格]确认 [Esc]取消' ;
} else {
badge . textContent = '拾取 · ' + tag + ( name ? ' · ' + name . slice ( 0 , 42 ) : '' ) + ' · [空格]锁定 [Esc]取消' ;
}
badge . style . left = Math . max ( 0 , rect . left ) + 'px' ;
badge . style . top = Math . max ( 0 , rect . top - 34 ) + 'px' ;
} ;
const interactiveSelector = [
'button' , 'a' , 'input' , 'select' , 'textarea' , 'summary' , 'label' , 'option' ,
'[role]' , '[onclick]' , '[contenteditable]' , '[tabindex]'
] . join ( ',' ) ;
const closestInteractive = ( start ) = > {
let node = start ;
while ( node && node . nodeType === 1 ) {
try { if ( node . matches ( interactiveSelector ) ) return node ; } catch { }
if ( node . parentElement ) {
node = node . parentElement ;
continue ;
}
const root = node . getRootNode ( ) ;
node = root instanceof ShadowRoot ? root.host : null ;
}
return start ;
} ;
const firstElement = ( event ) = > {
let deepest = null ;
for ( const item of event . composedPath ( ) ) {
if ( item instanceof Element ) {
deepest = item ;
break ;
}
}
if ( ! deepest && event . target instanceof Element ) deepest = event . target ;
return closestInteractive ( deepest ) ;
} ;
const buildChain = ( node ) = > {
const result = [ ] ;
let current = node ;
while ( current && current . nodeType === 1 ) {
result . unshift ( current ) ;
const root = current . getRootNode ( ) ;
current = root instanceof ShadowRoot ? root.host : null ;
}
return result ;
} ;
const lock = ( ) = > {
if ( locked || ! target ) return ;
locked = true ;
chain = buildChain ( target ) ;
chainIndex = Math . max ( 0 , chain . length - 1 ) ;
selected = chain [ chainIndex ] || target ;
updateBox ( ) ;
} ;
const adjustLevel = ( delta ) = > {
if ( ! locked || ! chain . length ) return ;
const next = Math . min ( chain . length - 1 , Math . max ( 0 , chainIndex + delta ) ) ;
if ( next === chainIndex ) return ;
chainIndex = next ;
selected = chain [ chainIndex ] ;
updateBox ( ) ;
} ;
const confirm = ( ) = > {
if ( ! locked || ! selected ) return ;
cleanup ( ) ;
resolve ( describe ( selected ) ) ;
} ;
const move = ( event ) = > {
if ( finished || locked ) return ;
target = firstElement ( event ) ;
updateBox ( ) ;
} ;
const click = ( event ) = > {
if ( finished ) return ;
// 与桌面拾取对齐:鼠标只用于定位,空格锁定/确认,避免误触发页面按钮。
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
event . stopImmediatePropagation ( ) ;
} ;
const key = ( event ) = > {
if ( event . key === 'Escape' ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
cleanup ( ) ;
reject ( new Error ( '已取消元素拾取' ) ) ;
return ;
}
if ( event . code === 'Space' && ! event . repeat ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
if ( locked ) confirm ( ) ;
else lock ( ) ;
return ;
}
if ( locked && event . key === 'PageUp' && ! event . repeat ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
adjustLevel ( - 1 ) ;
return ;
}
if ( locked && event . key === 'PageDown' && ! event . repeat ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
adjustLevel ( 1 ) ;
}
} ;
const cleanup = ( ) = > {
if ( finished ) return ;
finished = true ;
clearTimeout ( timer ) ;
window . removeEventListener ( 'mousemove' , move , true ) ;
window . removeEventListener ( 'mousedown' , click , true ) ;
window . removeEventListener ( 'click' , click , true ) ;
window . removeEventListener ( 'scroll' , updateBox , true ) ;
window . removeEventListener ( 'resize' , updateBox ) ;
window . removeEventListener ( 'keydown' , key , true ) ;
host ? . remove ( ) ;
delete window . __rpaElementPicker ;
} ;
host = document . createElement ( 'div' ) ;
host . style . cssText = 'position:fixed;inset:0;width:100vw;height:100vh;z-index:2147483647;pointer-events:none;' ;
root = host . attachShadow ( { mode : 'closed' } ) ;
box = document . createElement ( 'div' ) ;
badge = document . createElement ( 'div' ) ;
box . style . cssText = 'position:fixed;border:2px solid #2563eb;background:rgba(37,99,235,.16);border-radius:3px;pointer-events:none;' ;
badge . style . cssText = 'position:fixed;z-index:2147483647;padding:4px 7px;border-radius:6px;background:#2563eb;color:#fff;font:600 12px/16px system-ui,sans-serif;pointer-events:none;white-space:nowrap;' ;
root . append ( box , badge ) ;
document . documentElement . append ( host ) ;
window . addEventListener ( 'mousemove' , move , true ) ;
window . addEventListener ( 'mousedown' , click , true ) ;
window . addEventListener ( 'click' , click , true ) ;
window . addEventListener ( 'scroll' , updateBox , true ) ;
window . addEventListener ( 'resize' , updateBox ) ;
window . addEventListener ( 'keyup' , ( ) = > { } , true ) ;
window . addEventListener ( 'keydown' , key , true ) ;
window . __rpaElementPicker = { cancel : ( ) = > { cleanup ( ) ; reject ( new Error ( '已取消元素拾取' ) ) ; } } ;
timer = setTimeout ( ( ) = > {
cleanup ( ) ;
reject ( new Error ( '拾取超时,请重新点击拾取' ) ) ;
} , timeout ) ;
} ) ) ( ) ` );
return result ;
if ( ! state . active ) await inject ( ) . catch ( ( ) = > { } ) ; // 注入未生效时兜底重注入
}
throw new RuntimeError ( "timeout" , "拾取超时,请重新点击拾取" ) ;
} finally {
const cleanupExpr =
"(() => { try { window.__rpaElementPicker?.cancelHard?.(); window.__rpaPickerActive = undefined; delete window.__rpaPickResult; } catch {} })()" ;
await this . evaluate ( cleanupExpr ) . catch ( ( ) = > { } ) ;
for ( const tabTransport of this . tabTransports . values ( ) ) {
await tabTransport . send ( "Runtime.evaluate" , { expression : cleanupExpr } ) . catch ( ( ) = > { } ) ;
}
for ( const identifier of injectionIds ) {
await this . cdp ( "Page.removeScriptToEvaluateOnNewDocument" , { identifier } ) . catch ( ( ) = > { } ) ;
}
}
}
async close ( ) : Promise < void > {
@ -1429,6 +1277,299 @@ declare global {
var __rpaExternalSessions : Map < string , BrowserSession > | undefined ;
}
const PICKER_BOOTSTRAP_SOURCE = ` (() => {
if ( window . __rpaPickerActive && window . __rpaPickerActive . active ) {
// 强杀应用等异常场景会留下 active 残留拾取器, 若直接 return 新拾取永远不生效, 必须强制接管
try { window . __rpaElementPicker ? . cancelHard ? . ( ) ; window . __rpaElementPicker ? . cancel ? . ( ) ; } catch { }
window . __rpaPickerActive = undefined ;
document . getElementById ( '__rpa_pick_host' ) ? . remove ( ) ;
}
window . __rpaPickResult = null ;
let host = null ;
let root = null ;
let box = null ;
let badge = null ;
let target = null ;
let deepTarget = null ;
let lastSnap = null ;
let drilled = false ;
let hoverTimer = 0 ;
let locked = false ;
let chain = [ ] ;
let chainIndex = 0 ;
let selected = null ;
let finished = false ;
const cssEscape = ( value ) = > {
if ( globalThis . CSS && typeof CSS . escape === 'function' ) return CSS . escape ( String ( value ) ) ;
return String ( value ) . replace ( /[^a-zA-Z0-9_-]/g , ( char ) = > '\\\\' + char ) ;
} ;
const isUnique = ( candidate ) = > {
try { return document . querySelectorAll ( candidate ) . length === 1 ; } catch { return false ; }
} ;
const cssPath = ( node ) = > {
const localPath = ( start , root ) = > {
const stable = ( item ) = > {
if ( item . id ) {
// 长数字结尾的唯一 id 多为动态记录 id( 如淘宝 item_id_1087512368188) , 泛化为前缀匹配以命中所有同类卡片
const dynamic = /^(.*?[-_])[0-9]{6,}$/ . exec ( item . id ) ;
if ( dynamic ) {
const generalized = '[id^="' + cssEscape ( dynamic [ 1 ] ) + '"]' ;
try { if ( root . querySelectorAll ( generalized ) . length >= 1 ) return generalized ; } catch { }
}
const candidate = '#' + cssEscape ( item . id ) ;
try { if ( root . querySelectorAll ( candidate ) . length === 1 ) return candidate ; } catch { }
if ( dynamic ) {
const generalized = '[id^="' + cssEscape ( dynamic [ 1 ] ) + '"]' ;
try { if ( root . querySelectorAll ( generalized ) . length >= 1 ) return generalized ; } catch { }
}
}
const testId = item . getAttribute ( 'data-testid' ) ;
if ( testId ) {
const candidate = '[data-testid="' + cssEscape ( testId ) + '"]' ;
try { if ( root . querySelectorAll ( candidate ) . length === 1 ) return candidate ; } catch { }
}
return null ;
} ;
const parts = [ ] ;
let current = start ;
while ( current && current . nodeType === 1 ) {
const stableCandidate = stable ( current ) ;
if ( stableCandidate ) {
parts . unshift ( stableCandidate ) ;
return parts . join ( ' > ' ) ;
}
let part = current . tagName . toLowerCase ( ) ;
const parent = current . parentElement ;
if ( parent ) {
const siblings = [ . . . parent . children ] . filter ( ( item ) = > item . tagName === current . tagName ) ;
if ( siblings . length > 1 ) part += ':nth-of-type(' + ( siblings . indexOf ( current ) + 1 ) + ')' ;
}
parts . unshift ( part ) ;
current = parent ;
}
return parts . join ( ' > ' ) ;
} ;
const trees = [ ] ;
let current = node ;
while ( current && current . nodeType === 1 ) {
const root = current . getRootNode ( ) ;
trees . unshift ( localPath ( current , root ) ) ;
current = root instanceof ShadowRoot ? root.host : null ;
}
return trees . join ( ' >>> ' ) ;
} ;
const describe = ( node ) = > {
const rect = node . getBoundingClientRect ( ) ;
return {
selector : 'css:' + cssPath ( node ) ,
tag : node.tagName.toLowerCase ( ) ,
id : node.id || undefined ,
className : node.getAttribute ( 'class' ) || undefined ,
name : node.getAttribute ( 'name' ) || undefined ,
type : node . getAttribute ( 'type' ) || undefined ,
placeholder : node.getAttribute ( 'placeholder' ) || undefined ,
label : node.getAttribute ( 'aria-label' ) || node . getAttribute ( 'placeholder' ) || node . getAttribute ( 'name' ) || ( node . innerText || '' ) . trim ( ) . slice ( 0 , 100 ) || undefined ,
text : ( node . innerText || node . textContent || '' ) . trim ( ) . slice ( 0 , 200 ) || undefined ,
value : String ( node . value ? ? '' ) || undefined ,
url : location.href
} ;
} ;
const nodeName = ( node ) = > node . getAttribute ( 'aria-label' ) || node . getAttribute ( 'placeholder' ) || node . getAttribute ( 'name' ) || node . id || ( node . innerText || '' ) . trim ( ) . slice ( 0 , 60 ) || '' ;
const updateBox = ( ) = > {
const node = locked ? selected : target ;
if ( ! node || ! box || ! badge ) return ;
const rect = node . getBoundingClientRect ( ) ;
box . style . left = Math . max ( 0 , rect . left - 3 ) + 'px' ;
box . style . top = Math . max ( 0 , rect . top - 3 ) + 'px' ;
box . style . width = Math . max ( 4 , rect . width + 6 ) + 'px' ;
box . style . height = Math . max ( 4 , rect . height + 6 ) + 'px' ;
const tag = node . tagName . toLowerCase ( ) ;
const name = nodeName ( node ) ;
if ( locked ) {
badge . textContent = '已锁定 ' + ( chainIndex + 1 ) + '/' + chain . length + ' · ' + tag + ( name ? ' · ' + name . slice ( 0 , 42 ) : '' ) + ' · [PageUp]升 [PageDown]钻入子元素 [空格]确认 [Esc]取消' ;
} else {
badge . textContent = '拾取 · ' + tag + ( name ? ' · ' + name . slice ( 0 , 42 ) : '' ) + ' · 悬停0.3秒自动钻入 · [PageUp/PageDown]手动调层 [空格]确认 [Esc]取消 · 可直接翻页后继续拾取' ;
}
badge . style . left = Math . max ( 0 , rect . left ) + 'px' ;
badge . style . top = Math . max ( 0 , rect . top - 34 ) + 'px' ;
} ;
const interactiveSelector = [
'button' , 'a' , 'input' , 'select' , 'textarea' , 'summary' , 'label' , 'option' ,
'[role]' , '[onclick]' , '[contenteditable]' , '[tabindex]'
] . join ( ',' ) ;
const closestInteractive = ( start ) = > {
let node = start ;
while ( node && node . nodeType === 1 ) {
try { if ( node . matches ( interactiveSelector ) ) return node ; } catch { }
if ( node . parentElement ) {
node = node . parentElement ;
continue ;
}
const root = node . getRootNode ( ) ;
node = root instanceof ShadowRoot ? root.host : null ;
}
return start ;
} ;
const deepestOf = ( event ) = > {
let deepest = null ;
for ( const item of event . composedPath ( ) ) {
if ( item instanceof Element ) {
deepest = item ;
break ;
}
}
if ( ! deepest && event . target instanceof Element ) deepest = event . target ;
return deepest ;
} ;
const firstElement = ( event ) = > {
return closestInteractive ( deepestOf ( event ) ) ;
} ;
const buildChain = ( node ) = > {
const result = [ ] ;
let current = node ;
while ( current && current . nodeType === 1 ) {
result . unshift ( current ) ;
const root = current . getRootNode ( ) ;
current = root instanceof ShadowRoot ? root.host : null ;
}
return result ;
} ;
const lockAt = ( base ) = > {
if ( locked || ! base ) return ;
locked = true ;
chain = buildChain ( base ) ;
chainIndex = Math . max ( 0 , chain . length - 1 ) ;
// 把光标下最深处元素到锁定元素之间的子级路径并入链, PageDown/PageUp 可在卡片与其内部子元素间切换
if ( deepTarget && base !== deepTarget && base . contains ( deepTarget ) ) {
const below = [ ] ;
let current = deepTarget ;
while ( current && current !== base && base . contains ( current ) ) {
below . unshift ( current ) ;
const root = current . getRootNode ( ) ;
current = root instanceof ShadowRoot ? root.host : current.parentElement ;
}
chain = chain . concat ( below ) ;
}
selected = chain [ chainIndex ] || base ;
updateBox ( ) ;
} ;
const lock = ( ) = > lockAt ( target ) ;
const adjustLevel = ( delta ) = > {
if ( ! locked || ! chain . length ) return ;
const next = Math . min ( chain . length - 1 , Math . max ( 0 , chainIndex + delta ) ) ;
if ( next === chainIndex ) return ;
chainIndex = next ;
selected = chain [ chainIndex ] ;
updateBox ( ) ;
} ;
const confirm = ( ) = > {
if ( ! locked || ! selected ) return ;
finish ( { ok : true , value : describe ( selected ) } ) ;
} ;
const move = ( event ) = > {
if ( finished || locked ) return ;
deepTarget = deepestOf ( event ) ;
const snapped = firstElement ( event ) ;
if ( snapped !== lastSnap ) {
// 进入新的交互区域:先吸附交互元素,悬停 0.3 秒后自动钻入光标最深处
lastSnap = snapped ;
drilled = false ;
target = snapped ;
clearTimeout ( hoverTimer ) ;
if ( deepTarget && deepTarget !== snapped ) {
hoverTimer = setTimeout ( ( ) = > {
if ( finished || locked ) return ;
drilled = true ;
target = deepTarget ;
updateBox ( ) ;
} , 300 ) ;
}
updateBox ( ) ;
return ;
}
if ( drilled && deepTarget && snapped . contains ( deepTarget ) ) {
target = deepTarget ; // 已钻入:跟随光标在卡片内部移动
}
updateBox ( ) ;
} ;
const click = ( event ) = > {
if ( finished ) return ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
event . stopImmediatePropagation ( ) ;
} ;
const key = ( event ) = > {
if ( event . key === 'Escape' ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
finish ( { ok : false , reason : '已取消元素拾取' } ) ;
return ;
}
if ( event . code === 'Space' && ! event . repeat ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
if ( locked ) confirm ( ) ;
else lock ( ) ;
return ;
}
if ( event . key === 'PageDown' && ! event . repeat ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
if ( locked ) adjustLevel ( 1 ) ;
else lockAt ( deepTarget || target ) ; // 未锁定时直接锁定光标最深处(如卡片内的标题)
return ;
}
if ( event . key === 'PageUp' && ! event . repeat ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
if ( locked ) adjustLevel ( - 1 ) ;
else if ( target ) lockAt ( target ) ; // 未锁定时锁定吸附到的交互元素(如整张卡片)
}
} ;
const cleanup = ( ) = > {
if ( finished ) return ;
finished = true ;
clearTimeout ( hoverTimer ) ;
window . removeEventListener ( 'mousemove' , move , true ) ;
window . removeEventListener ( 'mousedown' , click , true ) ;
window . removeEventListener ( 'click' , click , true ) ;
window . removeEventListener ( 'scroll' , updateBox , true ) ;
window . removeEventListener ( 'resize' , updateBox ) ;
window . removeEventListener ( 'keydown' , key , true ) ;
host && host . remove ( ) ;
window . __rpaElementPicker = null ;
window . __rpaPickerActive = { active : false } ;
} ;
const finish = ( payload ) = > {
cleanup ( ) ;
window . __rpaPickResult = payload ;
} ;
host = document . createElement ( 'div' ) ;
host . id = '__rpa_pick_host' ;
host . style . cssText = 'position:fixed;inset:0;width:100vw;height:100vh;z-index:2147483647;pointer-events:none;' ;
root = host . attachShadow ( { mode : 'closed' } ) ;
box = document . createElement ( 'div' ) ;
badge = document . createElement ( 'div' ) ;
box . style . cssText = 'position:fixed;border:2px solid #2563eb;background:rgba(37,99,235,.16);border-radius:3px;pointer-events:none;' ;
badge . style . cssText = 'position:fixed;z-index:2147483647;padding:4px 7px;border-radius:6px;background:#2563eb;color:#fff;font:600 12px/16px system-ui,sans-serif;pointer-events:none;white-space:nowrap;' ;
root . append ( box , badge ) ;
document . documentElement . append ( host ) ;
window . addEventListener ( 'mousemove' , move , true ) ;
window . addEventListener ( 'mousedown' , click , true ) ;
window . addEventListener ( 'click' , click , true ) ;
window . addEventListener ( 'scroll' , updateBox , true ) ;
window . addEventListener ( 'resize' , updateBox ) ;
window . addEventListener ( 'keydown' , key , true ) ;
window . __rpaElementPicker = {
cancel : ( ) = > { if ( ! finished ) finish ( { ok : false , reason : '已取消元素拾取' } ) ; } ,
cancelHard : ( ) = > { cleanup ( ) ; }
} ;
window . __rpaPickerActive = { active : true } ;
} ) ( ) ; ` ;
function externalSessionCache ( ) : Map < string , BrowserSession > {
if ( ! globalThis . __rpaExternalSessions ) globalThis . __rpaExternalSessions = new Map ( ) ;
return globalThis . __rpaExternalSessions ;