dsh-browser-view 0.7.2 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ DSH 插件:共享 Playwright 浏览器自动化 + 原生界面实时画面面板
5
5
  | 模块 | 做什么 |
6
6
  | --- | --- |
7
7
  | 🧭 **浏览器自动化** | 共享 Playwright 浏览器:多标签、多 profile、隔离 context、下载/上传、Cookie、what 意图语义定位(`browser_click { what: "登录" }` 一次调用替代快照→查找→点击三轮,零命中返回相近候选 + SoM 看图兜底)、SoM 标徽点击(`mark: N`)、open Shadow DOM 组合树穿透、SPA 路由后 ref 软失效重锚定、点击效果验证(`effect: true`,点了没动静自动换候选)、跨 Frame/IFrame 穿透定位、视口感知动态快照(`viewportOnly`)、多模态 SoM 视口标徽(`som: true`)、批量表单智能填充(`browser_form_fill`)、增量快照(历史版本环形缓存)、抽取字段自动建议(action:suggest),38 个 `browser_*` 工具 |
8
- | 🖥️ **实时画面** | 注入 DSH 界面右下角悬浮面板:浏览器状态 + 实时缩略图,点开大屏模态框(2 秒轮询画面 + 访问历史);**双向协作**——agent 等待人工操作/敏感审批时显示交互横幅、执行点击/输入前目标元素琥珀色高亮预告(confirmMode:auto 时敏感动作由宿主层强制拦截弹审批,agent 无法绕过),**面板点选教学**(browser_pick_element:疑难元素用户在大图上点一下,直接拿到稳定 selector+指纹+ref),「最近回归」行与报告模态可一键重跑(agent 忙碌自动置灰) |
8
+ | 🖥️ **实时画面** | 注入 DSH 界面右下角悬浮面板:浏览器状态 + 实时缩略图,点开大屏模态框(CDP MJPEG 实时流,自动跟随活动标签换页/新开自愈,流不可用时回退轮询截图;访问历史 2 秒轮询);**双向协作**——agent 等待人工操作/敏感审批时显示交互横幅、执行点击/输入前目标元素琥珀色高亮预告(confirmMode:auto 时敏感动作由宿主层强制拦截弹审批,agent 无法绕过),**面板点选教学**(browser_pick_element:疑难元素用户在大图上点一下,直接拿到稳定 selector+指纹+ref),「最近回归」行与报告模态可一键重跑(agent 忙碌自动置灰) |
9
9
  | ✅ **回归测试闭环** | 录制回放 + 选择器自愈 + 参数化/数据驱动 + 失败自动重试(`retryFailed`)+ 视觉回归 + 运行历史/报告导出 + **一键导出原生 Playwright 规范代码(`action: "export"`)** + 面板一键重跑(`/runs/rerun`,agent 忙碌置灰) |
10
10
 
11
11
  设计原则:能做的一次性操作交给 agent 用 bash 完成,插件只做 bash 做不到的常驻浏览器自动化。唯一第三方运行时依赖是 `playwright-core`(复用系统已装的 Edge/Chrome,无需下载 Chromium),不依赖任何 `@deepseek-ai` 运行时包。
package/lib/client.js CHANGED
@@ -1,2 +1,2 @@
1
- window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=null;async function a(){if(i!==null)return i;try{let e=await fetch(`/__dsh-browser-view__/config`,{cache:`no-store`});if(e.ok){let t=await e.json();if(typeof t?.basePath==`string`&&t.basePath)return i=t.basePath,t.basePath}}catch{}return i=`/browser`,i}async function o(e){try{let t=await fetch(`${e}/status`,{cache:`no-store`});return t.ok?await t.json():null}catch{return null}}async function s(e){try{let t=await fetch(`${e}/history`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.history)?n.history:[]}catch{return[]}}async function c(e){try{let t=await fetch(`${e}/log`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.calls)?n.calls:[]}catch{return[]}}async function l(e){try{let t=await fetch(`${e}/runs/latest`,{cache:`no-store`});if(!t.ok)return{run:null,busy:!1};let n=await t.json();return{run:n?.run??null,busy:n?.busy===!0}}catch{return{run:null,busy:!1}}}async function u(e){try{let t=await fetch(`${e}/confirm`,{cache:`no-store`});if(!t.ok)return null;let n=await t.json();return n?.confirm??n?.pending??null}catch{return null}}async function d(e,t,n){try{return(await fetch(`${e}/confirm`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,decision:n})})).ok}catch{return!1}}async function f(e){try{let t=await fetch(`${e}/pick`,{cache:`no-store`});return t.ok?(await t.json())?.pick??null:null}catch{return null}}async function p(e,t,n,r,i,a){try{return(await fetch(`${e}/pick`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,x:n,y:r,imgW:i,imgH:a})})).ok}catch{return!1}}async function m(e,t,n){try{let r=await fetch(`${e}/runs/rerun`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...n?{retryOf:n}:{}})});if(!r.ok){let e=await r.text().catch(()=>``);return{ok:!1,status:r.status,error:e}}return{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}async function h(e){try{let t=await fetch(`${e}/recordings`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.recordings)?n.recordings:[]}catch{return[]}}async function g(e,t){try{let n=await fetch(`${e}/recordings/detail?name=${encodeURIComponent(t)}`,{cache:`no-store`});return n.ok?await n.json():null}catch{return null}}async function _(e,t){try{return(await fetch(`${e}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:t})})).ok}catch{return!1}}function v(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)([]),[s,u]=(0,r.useState)(Date.now()),[d,f]=(0,r.useState)(null),[p,h]=(0,r.useState)(!1),[g,_]=(0,r.useState)(``);return(0,r.useEffect)(()=>{let t=!0,r=async()=>{let[r,i,s]=await Promise.all([o(e),c(e),l(e)]);t&&(n(r),r?.open&&u(Date.now()),a(i.slice(0,8)),f(s.run),h(s.busy))};r();let i=setInterval(r,3e3);return()=>{t=!1,clearInterval(i)}},[e]),{status:t,ops:i,shotVersion:s,lastRun:d,agentBusy:p,rerunMsg:g,rerunLast:async()=>{let t=d;if(!t||p)return;let n=t.source.replace(/\(已从磁盘重载\)$/,``);if(!n||[`steps`,`recording`,`recent-log`].includes(n)){_(`该来源不是已保存录制,无法一键重跑`);return}_(`重跑中…`),h(!0);let r=await m(e,n,t.runId);r.ok?_(`重跑完成,报告稍后刷新`):(h(!1),_(r.error??`重跑失败`))}}}function y(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await u(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingConfirm:t,resolveConfirm:async(t,r)=>{await d(e,t,r)&&n(null)}}}function b(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await f(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingPick:t}}let x={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},S={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function C(){let[e,t]=(0,r.useState)(()=>typeof document<`u`&&document.body?.getAttribute(`data-ds-dark-theme`)!==null);return(0,r.useEffect)(()=>{if(typeof document>`u`)return;let e=new MutationObserver(()=>t(document.body?.getAttribute(`data-ds-dark-theme`)!==null));return e.observe(document.body,{attributes:!0,attributeFilter:[`data-ds-dark-theme`]}),()=>e.disconnect()},[]),e}function w(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function T({confirm:e,dark:t,onDecision:n}){return(0,r.createElement)(`div`,{style:{padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #ef4444`,background:t?`rgba(239, 68, 68, 0.12)`:`rgba(239, 68, 68, 0.07)`,color:t?`#fca5a5`:`#b91c1c`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 请求执行敏感操作,请审批`},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`⚠`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`,fontWeight:600}},`敏感操作待审批`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`}},e.reason,e.tool?` (${e.tool})`:``),(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,marginTop:`6px`}},(0,r.createElement)(`button`,{onClick:()=>n(`allowed`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`允许`),(0,r.createElement)(`button`,{onClick:()=>n(`denied`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#dc2626`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`拒绝`)))}function E({status:e,ops:t,lastRun:n,agentBusy:i,rerunMsg:a,pendingConfirm:o,pendingPick:s,shotSrc:c,streamSrc:l,dark:u,palette:d,onOpenModal:f,onOpenReport:p,onConfirmDecision:m,onRerunLast:h}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`320px`,maxHeight:`78vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${d.border}`,borderRadius:`12px`,background:u?`#181e29`:`#ffffff`,boxShadow:`0 8px 32px rgba(0, 0, 0, 0.3)`,padding:`12px`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,padding:`4px 0`}},(0,r.createElement)(`span`,{style:{width:`8px`,height:`8px`,borderRadius:`999px`,flex:`none`,background:e?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:d.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:d.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},e===null?`未启用`:e.open?`${e.tabs??1} 标签 · ${(e.url||``).slice(0,20)}`:`未打开`)),o?(0,r.createElement)(T,{confirm:o,dark:u,onDecision:m}):null,e?.pause?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 正在等待人工操作,请在浏览器窗口中完成`},(0,r.createElement)(`span`,{style:{flex:`none`}},`⏸`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待人工操作: ${e.pause.instruction}`)):null,s?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`,cursor:`pointer`},title:`点击放大实时画面,在画面上点选目标元素`,onClick:f},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${s.instruction}(点此放大画面)`)):null,e?.open?(0,r.createElement)(`img`,{src:l||c,onError:e=>{e.currentTarget.src!==c&&(e.currentTarget.src=c)},alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:f,style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${d.border}`,cursor:`pointer`,background:u?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:u?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${t.length} 条)`),t.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{flex:`none`,color:e.ok?`#10b981`:`#ef4444`,fontWeight:600}},e.ok?`✓`:`✗`),(0,r.createElement)(`span`,{style:{color:d.tertiary,fontSize:`11px`,flex:`none`}},w(e.ts)),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`},title:`${e.tool}${e.error?`: ${e.error}`:``}`},`${e.tool}${e.error?`: ${e.error}`:``}`)))),n!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,cursor:`pointer`},title:`runId=${n.runId} · 点击查看报告`,onClick:p},(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:n.failed===0?`#10b981`:`#ef4444`}},n.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${n.source}${n.row===void 0?``:` 行${n.row}`} ${n.passed}/${n.total}${n.healedCount>0?` · 自愈${n.healedCount}`:``}${n.retries!==void 0&&n.retries.recovered>0?` · 重试挽回${n.retries.recovered}`:``} · ${new Date(n.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`),a!==``&&(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},a),(0,r.createElement)(`button`,{type:`button`,title:i?`agent 忙碌,稍后可重跑`:`重跑 ${n.source}(结果进运行历史,标 retryOf)`,disabled:i,onClick:e=>{e.stopPropagation(),h()},style:{flex:`none`,height:`20px`,padding:`0 8px`,border:`1px solid ${d.border}`,borderRadius:`6px`,background:`transparent`,color:i?d.tertiary:d.secondary,fontSize:`11px`,cursor:i?`not-allowed`:`pointer`,opacity:i?.5:1}},i?`重跑(忙碌)`:`↻ 重跑`)))}function D({status:e,history:t,shotSrc:n,streamSrc:i,dark:a,palette:o,onClose:s,pendingPick:c,onPick:l}){(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&s()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[s]);let u=e=>{if(c==null||l===void 0)return;let t=e.currentTarget,n=t.getBoundingClientRect(),r=t.naturalWidth||n.width,i=t.naturalHeight||n.height;if(r<=0||i<=0)return;let a=Math.min(n.width/r,n.height/i),o=(n.width-r*a)/2,s=(n.height-i*a)/2,u=Math.min(Math.max((e.clientX-n.left-o)/a,0),r),d=Math.min(Math.max((e.clientY-n.top-s)/a,0),i);l(Math.round(u),Math.round(d),Math.round(r),Math.round(i))},d=c!=null;return(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&s()},style:{position:`fixed`,inset:0,zIndex:2147483001,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1100px, 94vw)`,maxHeight:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${o.border}`,borderRadius:`12px`,background:a?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,color:o.primary,fontSize:`14px`,fontWeight:600}},e?.open?`${e.title||``} (${e.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:s,style:{width:`28px`,height:`28px`,border:`1px solid ${o.border}`,borderRadius:`6px`,background:`transparent`,color:o.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),d?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 10px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:a?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:a?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${c.instruction}(在下方画面上点击目标元素)`)):null,e?.open?(0,r.createElement)(`img`,{src:i||n,onError:e=>{e.currentTarget.src!==n&&(e.currentTarget.src=n)},alt:`浏览器大图`,...d?{onClick:u}:{},style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:d?`2px dashed #f59e0b`:`1px solid ${o.border}`,background:a?`#10141c`:`#f8fafc`,cursor:d?`crosshair`:`default`}}):(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${o.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${t.length} 条,点击在新标签打开)`),t.map((e,t)=>(0,r.createElement)(`a`,{key:t,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:o.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:o.tertiary,flex:`none`}},w(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function O({base:e,dark:t,palette:n,onClose:i}){return(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&i()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[i]),(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&i()},style:{position:`fixed`,inset:0,zIndex:2147483002,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1000px, 94vw)`,height:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${n.border}`,borderRadius:`12px`,background:t?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:n.primary,fontSize:`14px`,fontWeight:600}},`回归测试报告 (最新运行)`),(0,r.createElement)(`a`,{href:`${e}/runs/report.html`,target:`_blank`,rel:`noopener noreferrer`,style:{color:n.secondary,fontSize:`12px`,textDecoration:`none`,padding:`4px 8px`,border:`1px solid ${n.border}`,borderRadius:`6px`}},`新标签页打开`),(0,r.createElement)(`button`,{type:`button`,onClick:i,style:{width:`28px`,height:`28px`,border:`1px solid ${n.border}`,borderRadius:`6px`,background:`transparent`,color:n.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),(0,r.createElement)(`iframe`,{src:`${e}/runs/report.html`,title:`回归测试报告`,style:{flex:1,width:`100%`,border:`1px solid ${n.border}`,borderRadius:`8px`,background:`#ffffff`}})))}function k(){let e=C(),t=e?S:x,[n,i]=(0,r.useState)(`/browser`),[o,c]=(0,r.useState)(!1),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[m,h]=(0,r.useState)([]);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&i(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{status:g,ops:_,shotVersion:w,lastRun:T,agentBusy:k,rerunMsg:A,rerunLast:j}=v(n),{pendingConfirm:M,resolveConfirm:N}=y(n),{pendingPick:P}=b(n);(0,r.useEffect)(()=>{if(!l)return;let e=!0,t=async()=>{let t=await s(n);e&&h(t)};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[l,n]);let F=`${n}/screenshot?v=${w}`,I=`${n}/stream`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>c(e=>!e),style:{position:`fixed`,right:`16px`,bottom:`16px`,width:`42px`,height:`42px`,borderRadius:`999px`,border:`1px solid ${t.border}`,background:e?`#1e2430`:`#ffffff`,color:t.primary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`18px`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.2)`,transition:`transform 0.15s ease, background 0.15s ease`,pointerEvents:`auto`}},`🌐`),o&&(0,r.createElement)(E,{status:g,ops:_,lastRun:T,agentBusy:k,rerunMsg:A,pendingConfirm:M,pendingPick:P,shotSrc:F,streamSrc:I,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{M&&N(M.id,e)},onRerunLast:j}),l&&(0,r.createElement)(D,{status:g,history:m,shotSrc:F,streamSrc:I,dark:e,palette:t,onClose:()=>u(!1),pendingPick:P,onPick:(e,t,r,i)=>{P&&p(n,P.id,e,t,r,i)}}),d&&(0,r.createElement)(O,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function A(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)(``),[o,s]=(0,r.useState)({});return(0,r.useEffect)(()=>{let t=!0;return n(null),a(``),h(e).then(e=>{t&&n(e)}).catch(e=>{t&&a(e instanceof Error?e.message:String(e))}),()=>{t=!1}},[e]),{list:t,error:i,expanded:o,toggleDetail:t=>{s(n=>{let r=n[t];if(r!==void 0&&r!==`loading`){let e={...n};return delete e[t],e}return g(e,t).then(e=>{s(n=>({...n,[t]:e??`failed`}))}).catch(()=>s(e=>({...e,[t]:`failed`}))),{...n,[t]:`loading`}})},remove:async t=>{await _(e,t)&&(n(e=>e===null?e:e.filter(e=>e.name!==t)),s(e=>{let n={...e};return delete n[t],n}))}}}function j(e){return{row:{display:`flex`,alignItems:`center`,gap:`8px`,minWidth:0},label:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,fontSize:`13px`,color:e.primary,cursor:`pointer`},meta:{color:e.tertiary,fontSize:`12px`,whiteSpace:`nowrap`},btn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.secondary,font:`inherit`,fontSize:`12px`,cursor:`pointer`},delBtn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.danger,font:`inherit`,fontSize:`12px`,cursor:`pointer`},itemBox:{display:`flex`,flexDirection:`column`,gap:`6px`,border:`1px solid ${e.border}`,borderRadius:`8px`,padding:`8px 10px`},stepsBox:{display:`flex`,flexDirection:`column`,gap:`2px`,borderTop:`1px solid ${e.border}`,paddingTop:`6px`},stepLine:{display:`flex`,gap:`8px`,fontSize:`13px`,lineHeight:`1.7`,color:e.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`},toolName:{color:e.primary,flex:`none`,fontWeight:500},stepArgs:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,color:e.secondary},empty:{color:e.tertiary,fontSize:`12px`,textAlign:`center`,padding:`12px 0`},root:{display:`flex`,flexDirection:`column`,gap:`8px`,maxWidth:`720px`},heading:{color:e.tertiary,fontSize:`12px`,lineHeight:`1.6`}}}function M(e){let t=C(),n=t?S:x,i=j(n),[o,s]=(0,r.useState)(`/browser`);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&s(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{list:c,error:l,expanded:u,toggleDetail:d,remove:f}=A(o);return(0,r.createElement)(`div`,{style:i.root},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,paddingBottom:`12px`,marginBottom:`6px`,borderBottom:`1px solid ${n.border}`}},(0,r.createElement)(`div`,{style:{width:`36px`,height:`36px`,borderRadius:`8px`,background:t?`rgba(99, 102, 241, 0.15)`:`rgba(99, 102, 241, 0.1)`,border:`1px solid ${t?`rgba(99, 102, 241, 0.3)`:`rgba(99, 102, 241, 0.2)`}`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`20px`,flex:`none`}},`🌐`),(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`2px`}},(0,r.createElement)(`div`,{style:{fontSize:`15px`,fontWeight:600,color:n.primary}},`浏览器自动化与录制管理`),(0,r.createElement)(`div`,{style:{fontSize:`12px`,color:n.tertiary}},`管理已保存的浏览器回归脚本,查看执行序列与历史数据`))),(0,r.createElement)(`div`,{style:i.heading},`已保存的录制(由 browser_record save 生成,供 browser_replay 一键回放):`),l?(0,r.createElement)(`div`,{style:{...i.empty,color:n.danger}},`加载失败: ${l}`):c===null?(0,r.createElement)(`div`,{style:i.empty},`加载中…`):c.length===0?(0,r.createElement)(`div`,{style:i.empty},`暂无已保存录制。在与 Agent 对话中完成一轮浏览器操作后,输入「保存录制为 xxx」即可沉淀为回归用例。`):c.map(e=>{let t=u[e.name],n=t!==void 0&&t!==`loading`;return(0,r.createElement)(`div`,{key:e.name,style:i.itemBox},(0,r.createElement)(`div`,{style:i.row},(0,r.createElement)(`span`,{style:i.label,title:`点击展开 / 折叠步骤详情`,onClick:()=>d(e.name)},(n?`▾ `:`▸ `)+e.name),(0,r.createElement)(`span`,{style:i.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleDateString(`zh-CN`)}`),(0,r.createElement)(`button`,{type:`button`,style:i.btn,onClick:()=>d(e.name)},n?`收起`:`步骤`),(0,r.createElement)(`button`,{type:`button`,style:i.delBtn,onClick:()=>f(e.name)},`删除`)),n&&t!==`failed`&&(0,r.createElement)(`div`,{style:i.stepsBox},t.steps.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:i.stepLine},(0,r.createElement)(`span`,{style:i.meta},`#${t+1}`),(0,r.createElement)(`span`,{style:i.toolName},e.tool),(0,r.createElement)(`span`,{style:i.stepArgs},JSON.stringify(e.args))))))}))}let N=[`slots`];function P(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},M)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},k))}return n.apply=P,n.inject=N,t.exports}});
1
+ window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=null;async function a(){if(i!==null)return i;try{let e=await fetch(`/__dsh-browser-view__/config`,{cache:`no-store`});if(e.ok){let t=await e.json();if(typeof t?.basePath==`string`&&t.basePath)return i=t.basePath,t.basePath}}catch{}return i=`/browser`,i}async function o(e){try{let t=await fetch(`${e}/status`,{cache:`no-store`});return t.ok?await t.json():null}catch{return null}}async function s(e){try{let t=await fetch(`${e}/history`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.history)?n.history:[]}catch{return[]}}async function c(e){try{let t=await fetch(`${e}/log`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.calls)?n.calls:[]}catch{return[]}}async function l(e){try{let t=await fetch(`${e}/runs/latest`,{cache:`no-store`});if(!t.ok)return{run:null,busy:!1};let n=await t.json();return{run:n?.run??null,busy:n?.busy===!0}}catch{return{run:null,busy:!1}}}async function u(e){try{let t=await fetch(`${e}/confirm`,{cache:`no-store`});if(!t.ok)return null;let n=await t.json();return n?.confirm??n?.pending??null}catch{return null}}async function d(e,t,n){try{return(await fetch(`${e}/confirm`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,decision:n})})).ok}catch{return!1}}async function f(e){try{let t=await fetch(`${e}/pick`,{cache:`no-store`});return t.ok?(await t.json())?.pick??null:null}catch{return null}}async function p(e,t,n,r,i,a){try{return(await fetch(`${e}/pick`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,x:n,y:r,imgW:i,imgH:a})})).ok}catch{return!1}}async function m(e,t,n){try{let r=await fetch(`${e}/runs/rerun`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...n?{retryOf:n}:{}})});if(!r.ok){let e=await r.text().catch(()=>``);return{ok:!1,status:r.status,error:e}}return{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}async function h(e){try{let t=await fetch(`${e}/recordings`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.recordings)?n.recordings:[]}catch{return[]}}async function g(e,t){try{let n=await fetch(`${e}/recordings/detail?name=${encodeURIComponent(t)}`,{cache:`no-store`});return n.ok?await n.json():null}catch{return null}}async function _(e,t){try{return(await fetch(`${e}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:t})})).ok}catch{return!1}}function v(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)([]),[s,u]=(0,r.useState)(Date.now()),[d,f]=(0,r.useState)(null),[p,h]=(0,r.useState)(!1),[g,_]=(0,r.useState)(``);return(0,r.useEffect)(()=>{let t=!0,r=async()=>{let[r,i,s]=await Promise.all([o(e),c(e),l(e)]);t&&(n(r),r?.open&&u(Date.now()),a(i.slice(0,8)),f(s.run),h(s.busy))};r();let i=setInterval(r,3e3);return()=>{t=!1,clearInterval(i)}},[e]),{status:t,ops:i,shotVersion:s,lastRun:d,agentBusy:p,rerunMsg:g,rerunLast:async()=>{let t=d;if(!t||p)return;let n=t.source.replace(/\(已从磁盘重载\)$/,``);if(!n||[`steps`,`recording`,`recent-log`].includes(n)){_(`该来源不是已保存录制,无法一键重跑`);return}_(`重跑中…`),h(!0);let r=await m(e,n,t.runId);r.ok?_(`重跑完成,报告稍后刷新`):(h(!1),_(r.error??`重跑失败`))}}}function y(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await u(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingConfirm:t,resolveConfirm:async(t,r)=>{await d(e,t,r)&&n(null)}}}function b(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await f(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingPick:t}}let x={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},S={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function C(){let[e,t]=(0,r.useState)(()=>typeof document<`u`&&document.body?.getAttribute(`data-ds-dark-theme`)!==null);return(0,r.useEffect)(()=>{if(typeof document>`u`)return;let e=new MutationObserver(()=>t(document.body?.getAttribute(`data-ds-dark-theme`)!==null));return e.observe(document.body,{attributes:!0,attributeFilter:[`data-ds-dark-theme`]}),()=>e.disconnect()},[]),e}function w(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function T({confirm:e,dark:t,onDecision:n}){return(0,r.createElement)(`div`,{style:{padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #ef4444`,background:t?`rgba(239, 68, 68, 0.12)`:`rgba(239, 68, 68, 0.07)`,color:t?`#fca5a5`:`#b91c1c`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 请求执行敏感操作,请审批`},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`⚠`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`,fontWeight:600}},`敏感操作待审批`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`}},e.reason,e.tool?` (${e.tool})`:``),(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,marginTop:`6px`}},(0,r.createElement)(`button`,{onClick:()=>n(`allowed`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`允许`),(0,r.createElement)(`button`,{onClick:()=>n(`denied`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#dc2626`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`拒绝`)))}function E({streamSrc:e,shotSrc:t,alt:n,title:i,style:a,onClick:o}){let[s,c]=(0,r.useState)(!0),[l,u]=(0,r.useState)(0);(0,r.useEffect)(()=>{s||(u(e=>e+1),c(!0))},[t,s]);let d=s&&e!==void 0?l>0?`${e}${e.includes(`?`)?`&`:`?`}probe=${l}`:e:t;return(0,r.createElement)(`img`,{src:d,onError:()=>c(!1),alt:n,title:i,onClick:o,style:a})}function D({status:e,ops:t,lastRun:n,agentBusy:i,rerunMsg:a,pendingConfirm:o,pendingPick:s,shotSrc:c,streamSrc:l,dark:u,palette:d,onOpenModal:f,onOpenReport:p,onConfirmDecision:m,onRerunLast:h}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`320px`,maxHeight:`78vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${d.border}`,borderRadius:`12px`,background:u?`#181e29`:`#ffffff`,boxShadow:`0 8px 32px rgba(0, 0, 0, 0.3)`,padding:`12px`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,padding:`4px 0`}},(0,r.createElement)(`span`,{style:{width:`8px`,height:`8px`,borderRadius:`999px`,flex:`none`,background:e?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:d.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:d.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},e===null?`未启用`:e.open?`${e.tabs??1} 标签 · ${(e.url||``).slice(0,20)}`:`未打开`)),o?(0,r.createElement)(T,{confirm:o,dark:u,onDecision:m}):null,e?.pause?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 正在等待人工操作,请在浏览器窗口中完成`},(0,r.createElement)(`span`,{style:{flex:`none`}},`⏸`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待人工操作: ${e.pause.instruction}`)):null,s?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`,cursor:`pointer`},title:`点击放大实时画面,在画面上点选目标元素`,onClick:f},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${s.instruction}(点此放大画面)`)):null,e?.open?(0,r.createElement)(E,{streamSrc:l,shotSrc:c,alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:f,style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${d.border}`,cursor:`pointer`,background:u?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:u?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${t.length} 条)`),t.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{flex:`none`,color:e.ok?`#10b981`:`#ef4444`,fontWeight:600}},e.ok?`✓`:`✗`),(0,r.createElement)(`span`,{style:{color:d.tertiary,fontSize:`11px`,flex:`none`}},w(e.ts)),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`},title:`${e.tool}${e.error?`: ${e.error}`:``}`},`${e.tool}${e.error?`: ${e.error}`:``}`)))),n!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,cursor:`pointer`},title:`runId=${n.runId} · 点击查看报告`,onClick:p},(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:n.failed===0?`#10b981`:`#ef4444`}},n.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${n.source}${n.row===void 0?``:` 行${n.row}`} ${n.passed}/${n.total}${n.healedCount>0?` · 自愈${n.healedCount}`:``}${n.retries!==void 0&&n.retries.recovered>0?` · 重试挽回${n.retries.recovered}`:``} · ${new Date(n.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`),a!==``&&(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},a),(0,r.createElement)(`button`,{type:`button`,title:i?`agent 忙碌,稍后可重跑`:`重跑 ${n.source}(结果进运行历史,标 retryOf)`,disabled:i,onClick:e=>{e.stopPropagation(),h()},style:{flex:`none`,height:`20px`,padding:`0 8px`,border:`1px solid ${d.border}`,borderRadius:`6px`,background:`transparent`,color:i?d.tertiary:d.secondary,fontSize:`11px`,cursor:i?`not-allowed`:`pointer`,opacity:i?.5:1}},i?`重跑(忙碌)`:`↻ 重跑`)))}function O({status:e,history:t,shotSrc:n,streamSrc:i,dark:a,palette:o,onClose:s,pendingPick:c,onPick:l}){(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&s()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[s]);let u=e=>{if(c==null||l===void 0)return;let t=e.currentTarget,n=t.getBoundingClientRect(),r=t.naturalWidth||n.width,i=t.naturalHeight||n.height;if(r<=0||i<=0)return;let a=Math.min(n.width/r,n.height/i),o=(n.width-r*a)/2,s=(n.height-i*a)/2,u=Math.min(Math.max((e.clientX-n.left-o)/a,0),r),d=Math.min(Math.max((e.clientY-n.top-s)/a,0),i);l(Math.round(u),Math.round(d),Math.round(r),Math.round(i))},d=c!=null;return(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&s()},style:{position:`fixed`,inset:0,zIndex:2147483001,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1100px, 94vw)`,maxHeight:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${o.border}`,borderRadius:`12px`,background:a?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,color:o.primary,fontSize:`14px`,fontWeight:600}},e?.open?`${e.title||``} (${e.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:s,style:{width:`28px`,height:`28px`,border:`1px solid ${o.border}`,borderRadius:`6px`,background:`transparent`,color:o.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),d?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 10px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:a?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:a?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${c.instruction}(在下方画面上点击目标元素)`)):null,e?.open?(0,r.createElement)(E,{streamSrc:i,shotSrc:n,alt:`浏览器大图`,...d?{onClick:u}:{},style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:d?`2px dashed #f59e0b`:`1px solid ${o.border}`,background:a?`#10141c`:`#f8fafc`,cursor:d?`crosshair`:`default`}}):(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${o.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${t.length} 条,点击在新标签打开)`),t.map((e,t)=>(0,r.createElement)(`a`,{key:t,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:o.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:o.tertiary,flex:`none`}},w(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function k({base:e,dark:t,palette:n,onClose:i}){return(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&i()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[i]),(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&i()},style:{position:`fixed`,inset:0,zIndex:2147483002,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1000px, 94vw)`,height:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${n.border}`,borderRadius:`12px`,background:t?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:n.primary,fontSize:`14px`,fontWeight:600}},`回归测试报告 (最新运行)`),(0,r.createElement)(`a`,{href:`${e}/runs/report.html`,target:`_blank`,rel:`noopener noreferrer`,style:{color:n.secondary,fontSize:`12px`,textDecoration:`none`,padding:`4px 8px`,border:`1px solid ${n.border}`,borderRadius:`6px`}},`新标签页打开`),(0,r.createElement)(`button`,{type:`button`,onClick:i,style:{width:`28px`,height:`28px`,border:`1px solid ${n.border}`,borderRadius:`6px`,background:`transparent`,color:n.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),(0,r.createElement)(`iframe`,{src:`${e}/runs/report.html`,title:`回归测试报告`,style:{flex:1,width:`100%`,border:`1px solid ${n.border}`,borderRadius:`8px`,background:`#ffffff`}})))}function A(){let e=C(),t=e?S:x,[n,i]=(0,r.useState)(`/browser`),[o,c]=(0,r.useState)(!1),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[m,h]=(0,r.useState)([]);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&i(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{status:g,ops:_,shotVersion:w,lastRun:T,agentBusy:E,rerunMsg:A,rerunLast:j}=v(n),{pendingConfirm:M,resolveConfirm:N}=y(n),{pendingPick:P}=b(n);(0,r.useEffect)(()=>{if(!l)return;let e=!0,t=async()=>{let t=await s(n);e&&h(t)};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[l,n]);let F=`${n}/screenshot?v=${w}`,I=`${n}/stream`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>c(e=>!e),style:{position:`fixed`,right:`16px`,bottom:`16px`,width:`42px`,height:`42px`,borderRadius:`999px`,border:`1px solid ${t.border}`,background:e?`#1e2430`:`#ffffff`,color:t.primary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`18px`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.2)`,transition:`transform 0.15s ease, background 0.15s ease`,pointerEvents:`auto`}},`🌐`),o&&(0,r.createElement)(D,{status:g,ops:_,lastRun:T,agentBusy:E,rerunMsg:A,pendingConfirm:M,pendingPick:P,shotSrc:F,streamSrc:I,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{M&&N(M.id,e)},onRerunLast:j}),l&&(0,r.createElement)(O,{status:g,history:m,shotSrc:F,streamSrc:I,dark:e,palette:t,onClose:()=>u(!1),pendingPick:P,onPick:(e,t,r,i)=>{P&&p(n,P.id,e,t,r,i)}}),d&&(0,r.createElement)(k,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function j(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)(``),[o,s]=(0,r.useState)({});return(0,r.useEffect)(()=>{let t=!0;return n(null),a(``),h(e).then(e=>{t&&n(e)}).catch(e=>{t&&a(e instanceof Error?e.message:String(e))}),()=>{t=!1}},[e]),{list:t,error:i,expanded:o,toggleDetail:t=>{s(n=>{let r=n[t];if(r!==void 0&&r!==`loading`){let e={...n};return delete e[t],e}return g(e,t).then(e=>{s(n=>({...n,[t]:e??`failed`}))}).catch(()=>s(e=>({...e,[t]:`failed`}))),{...n,[t]:`loading`}})},remove:async t=>{await _(e,t)&&(n(e=>e===null?e:e.filter(e=>e.name!==t)),s(e=>{let n={...e};return delete n[t],n}))}}}function M(e){return{row:{display:`flex`,alignItems:`center`,gap:`8px`,minWidth:0},label:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,fontSize:`13px`,color:e.primary,cursor:`pointer`},meta:{color:e.tertiary,fontSize:`12px`,whiteSpace:`nowrap`},btn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.secondary,font:`inherit`,fontSize:`12px`,cursor:`pointer`},delBtn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.danger,font:`inherit`,fontSize:`12px`,cursor:`pointer`},itemBox:{display:`flex`,flexDirection:`column`,gap:`6px`,border:`1px solid ${e.border}`,borderRadius:`8px`,padding:`8px 10px`},stepsBox:{display:`flex`,flexDirection:`column`,gap:`2px`,borderTop:`1px solid ${e.border}`,paddingTop:`6px`},stepLine:{display:`flex`,gap:`8px`,fontSize:`13px`,lineHeight:`1.7`,color:e.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`},toolName:{color:e.primary,flex:`none`,fontWeight:500},stepArgs:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,color:e.secondary},empty:{color:e.tertiary,fontSize:`12px`,textAlign:`center`,padding:`12px 0`},root:{display:`flex`,flexDirection:`column`,gap:`8px`,maxWidth:`720px`},heading:{color:e.tertiary,fontSize:`12px`,lineHeight:`1.6`}}}function N(e){let t=C(),n=t?S:x,i=M(n),[o,s]=(0,r.useState)(`/browser`);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&s(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{list:c,error:l,expanded:u,toggleDetail:d,remove:f}=j(o);return(0,r.createElement)(`div`,{style:i.root},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,paddingBottom:`12px`,marginBottom:`6px`,borderBottom:`1px solid ${n.border}`}},(0,r.createElement)(`div`,{style:{width:`36px`,height:`36px`,borderRadius:`8px`,background:t?`rgba(99, 102, 241, 0.15)`:`rgba(99, 102, 241, 0.1)`,border:`1px solid ${t?`rgba(99, 102, 241, 0.3)`:`rgba(99, 102, 241, 0.2)`}`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`20px`,flex:`none`}},`🌐`),(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`2px`}},(0,r.createElement)(`div`,{style:{fontSize:`15px`,fontWeight:600,color:n.primary}},`浏览器自动化与录制管理`),(0,r.createElement)(`div`,{style:{fontSize:`12px`,color:n.tertiary}},`管理已保存的浏览器回归脚本,查看执行序列与历史数据`))),(0,r.createElement)(`div`,{style:i.heading},`已保存的录制(由 browser_record save 生成,供 browser_replay 一键回放):`),l?(0,r.createElement)(`div`,{style:{...i.empty,color:n.danger}},`加载失败: ${l}`):c===null?(0,r.createElement)(`div`,{style:i.empty},`加载中…`):c.length===0?(0,r.createElement)(`div`,{style:i.empty},`暂无已保存录制。在与 Agent 对话中完成一轮浏览器操作后,输入「保存录制为 xxx」即可沉淀为回归用例。`):c.map(e=>{let t=u[e.name],n=t!==void 0&&t!==`loading`;return(0,r.createElement)(`div`,{key:e.name,style:i.itemBox},(0,r.createElement)(`div`,{style:i.row},(0,r.createElement)(`span`,{style:i.label,title:`点击展开 / 折叠步骤详情`,onClick:()=>d(e.name)},(n?`▾ `:`▸ `)+e.name),(0,r.createElement)(`span`,{style:i.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleDateString(`zh-CN`)}`),(0,r.createElement)(`button`,{type:`button`,style:i.btn,onClick:()=>d(e.name)},n?`收起`:`步骤`),(0,r.createElement)(`button`,{type:`button`,style:i.delBtn,onClick:()=>f(e.name)},`删除`)),n&&t!==`failed`&&(0,r.createElement)(`div`,{style:i.stepsBox},t.steps.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:i.stepLine},(0,r.createElement)(`span`,{style:i.meta},`#${t+1}`),(0,r.createElement)(`span`,{style:i.toolName},e.tool),(0,r.createElement)(`span`,{style:i.stepArgs},JSON.stringify(e.args))))))}))}let P=[`slots`];function F(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},N)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},A))}return n.apply=F,n.inject=P,t.exports}});
2
2
  //# sourceMappingURL=client.js.map
package/lib/index.js CHANGED
@@ -572,12 +572,12 @@ function __walkerCore() {
572
572
  }
573
573
  if (el.id) {
574
574
  const t = document.querySelector(`label[for="${CSS.escape(el.id)}"]`)?.textContent?.trim();
575
- if (t) return t.slice(0, 60);
575
+ if (t) return t.replace(/^[*::\s]+|[*::\s]+$/g, "").slice(0, 60);
576
576
  }
577
577
  const wrapLabel = el.closest("label");
578
578
  if (wrapLabel) {
579
579
  const t = wrapLabel.textContent?.trim();
580
- if (t) return t.slice(0, 60);
580
+ if (t) return t.replace(/^[*::\s]+|[*::\s]+$/g, "").slice(0, 60);
581
581
  }
582
582
  if ([
583
583
  "checkbox",
@@ -587,17 +587,45 @@ function __walkerCore() {
587
587
  const container = el.closest("label, [class*='wrapper'], [class*='item'], li");
588
588
  if (container) {
589
589
  const t = container.textContent?.trim();
590
- if (t) return t.replace(/\s+/g, " ").slice(0, 60);
590
+ if (t) return t.replace(/\s+/g, " ").replace(/^[*::\s]+|[*::\s]+$/g, "").slice(0, 60);
591
591
  }
592
592
  }
593
593
  if ([
594
594
  "textbox",
595
595
  "searchbox",
596
596
  "combobox",
597
- "spinbutton"
597
+ "spinbutton",
598
+ "slider",
599
+ "listbox"
598
600
  ].includes(role)) {
601
+ const formItem = el.closest("[class*='form-item'], [class*='form__item'], [class*='form-group'], [role='group']");
602
+ if (formItem) {
603
+ const lab = formItem.querySelector("label, [class*='form-item-label'], [class*='form-item__label'], [class*='label']");
604
+ if (lab) {
605
+ const t = (lab.getAttribute("title") || lab.textContent || "").trim().replace(/^[*::\s]+|[*::\s]+$/g, "");
606
+ if (t) return t.slice(0, 60);
607
+ }
608
+ }
609
+ const selectHost = el.closest("[class*='select'], [class*='cascader'], [class*='picker']");
610
+ if (selectHost) {
611
+ const ph = selectHost.querySelector("[class*='placeholder'], [class*='selection-placeholder']")?.textContent?.trim();
612
+ if (ph) return ph.slice(0, 60);
613
+ const selVal = selectHost.querySelector("[class*='selection-item'], [class*='selected-value']")?.textContent?.trim();
614
+ if (selVal) return selVal.slice(0, 60);
615
+ }
599
616
  const ph = el.getAttribute("placeholder");
600
617
  if (ph && ph.trim()) return ph.trim().slice(0, 60);
618
+ const host = selectHost || el;
619
+ const prevEl = host.previousElementSibling;
620
+ if (prevEl) {
621
+ const prevText = (prevEl.textContent || "").trim().replace(/^[*::\s]+|[*::\s]+$/g, "");
622
+ if (prevText && prevText.length <= 30) return prevText.slice(0, 60);
623
+ }
624
+ const td = host.closest("td");
625
+ if (td && td.previousElementSibling) {
626
+ const tdText = (td.previousElementSibling.textContent || "").trim().replace(/^[*::\s]+|[*::\s]+$/g, "");
627
+ if (tdText && tdText.length <= 30) return tdText.slice(0, 60);
628
+ }
601
629
  }
602
630
  const text = (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ");
603
631
  if (text) return text.slice(0, 60);
@@ -1376,8 +1404,38 @@ async function executeUniversalSelect(ctx) {
1376
1404
  const options = await withTimeout(target.locator("option").allTextContents(), probe, "读取原生下拉选项").catch(() => []);
1377
1405
  throw new Error(`原生下拉无选项 "${want}"; 可用选项: ${options.map((t) => t.trim()).join(" / ")}`);
1378
1406
  }
1379
- const hostLocator = target.locator("xpath=ancestor-or-self::*[@role='combobox' or contains(@class, 'select') or contains(@class, 'selector')][last()]");
1380
- const actualTarget = await hostLocator.count().catch(() => 0) > 0 ? hostLocator.first() : target;
1407
+ let resolvedTarget = target;
1408
+ if (await withTimeout(target.evaluate((el) => {
1409
+ if (el.tagName.toLowerCase() === "label") return true;
1410
+ const cls = typeof el.className === "string" ? el.className : "";
1411
+ if (/(?:form-item|form__item|form-group)/i.test(cls)) return true;
1412
+ if (el.closest("label, [class*='form-item'], [class*='form__item'], [class*='form-group']")) return !el.matches("[role='combobox'], select, [class*='select-selector']");
1413
+ if (!el.matches("[role='combobox'], select, [class*='select'], [class*='selector']")) return true;
1414
+ return false;
1415
+ }), 1500, "探测 label 或非标文本容器").catch(() => false)) {
1416
+ const descendantSelect = target.locator("xpath=.//*[@role='combobox' or self::select or contains(@class, 'select-selector') or contains(@class, 'select')][not(self::label)]").first();
1417
+ if (await descendantSelect.count().catch(() => 0) > 0) resolvedTarget = descendantSelect;
1418
+ else {
1419
+ const forId = await target.getAttribute("for").catch(() => null);
1420
+ if (forId) {
1421
+ const forTarget = page.locator(`[id="${forId.replace(/"/g, "\\\"")}"]`).first();
1422
+ if (await forTarget.count().catch(() => 0) > 0) resolvedTarget = forTarget;
1423
+ } else {
1424
+ const nextSiblingSelect = target.locator("xpath=following-sibling::*[self::select or @role='combobox' or contains(@class, 'select') or contains(@class, 'selector')][1] | following-sibling::*[1]//*[@role='combobox' or self::select or contains(@class, 'select')]").first();
1425
+ if (await nextSiblingSelect.count().catch(() => 0) > 0) resolvedTarget = nextSiblingSelect;
1426
+ else {
1427
+ const tdSiblingSelect = target.locator("xpath=ancestor-or-self::td[1]/following-sibling::td[1]//*[@role='combobox' or self::select or contains(@class, 'select')]").first();
1428
+ if (await tdSiblingSelect.count().catch(() => 0) > 0) resolvedTarget = tdSiblingSelect;
1429
+ else {
1430
+ const siblingSelect = target.locator("xpath=ancestor-or-self::*[contains(@class, 'form-item') or contains(@class, 'form__item') or contains(@class, 'form-group')][1]//*[@role='combobox' or self::select or contains(@class, 'select-selector') or contains(@class, 'select')][not(self::label)] | ..//*[@role='combobox' or self::select or contains(@class, 'select-selector') or contains(@class, 'select')][not(self::label)]").first();
1431
+ if (await siblingSelect.count().catch(() => 0) > 0) resolvedTarget = siblingSelect;
1432
+ }
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ const hostLocator = resolvedTarget.locator("xpath=ancestor-or-self::*[@role='combobox' or contains(@class, 'select') or contains(@class, 'selector')][last()]");
1438
+ const actualTarget = await hostLocator.count().catch(() => 0) > 0 ? hostLocator.first() : resolvedTarget;
1381
1439
  if (clear) try {
1382
1440
  const clearBtn = actualTarget.locator("[class*='clear'], [aria-label*='clear']").first();
1383
1441
  if (await clearBtn.count().catch(() => 0) > 0 && await clearBtn.isVisible().catch(() => false)) await clearBtn.click({ timeout: 1e3 }).catch(() => {});
@@ -1430,13 +1488,17 @@ async function executeUniversalSelect(ctx) {
1430
1488
  const optionSelector = "[role='option']:not([style*='height: 0px']):not([class*='empty']), [class*='item-option']:not([class*='empty']), [class*='select-dropdown'] li:not([class*='empty'])";
1431
1489
  const dropdownSelector = "[role='listbox']:not([style*='display: none']), [class*='select-dropdown']:not([style*='display: none']):not([class*='hidden']), [class*='popper']:not([style*='display: none'])";
1432
1490
  let options;
1491
+ let targetDropdown = null;
1433
1492
  if (controlsId) {
1434
1493
  const parentDropdown = page.locator(`xpath=//*[@id='${controlsId}']/ancestor-or-self::*[contains(@class, 'dropdown') or contains(@class, 'popper') or @role='listbox'][1] | //*[contains(@class, 'dropdown') or @role='listbox'][.//*[@id='${controlsId}']]`);
1494
+ targetDropdown = parentDropdown.first();
1435
1495
  const visual = parentDropdown.locator(optionSelector).locator("visible=true");
1436
1496
  options = await visual.count().catch(() => 0) > 0 ? visual : parentDropdown.locator(optionSelector);
1437
1497
  } else {
1438
- const activeDropdowns = page.locator(dropdownSelector);
1439
- const dropdown = await activeDropdowns.count().catch(() => 0) > 1 ? activeDropdowns.last() : activeDropdowns.first();
1498
+ const activeVisualDropdowns = page.locator(dropdownSelector).locator("visible=true");
1499
+ const visualCount = await activeVisualDropdowns.count().catch(() => 0);
1500
+ const dropdown = visualCount > 1 ? activeVisualDropdowns.last() : visualCount === 1 ? activeVisualDropdowns.first() : page.locator(dropdownSelector).last();
1501
+ targetDropdown = dropdown;
1440
1502
  const visual = dropdown.locator(optionSelector).locator("visible=true");
1441
1503
  options = await visual.count().catch(() => 0) > 0 ? visual : dropdown.locator(optionSelector);
1442
1504
  }
@@ -1452,7 +1514,7 @@ async function executeUniversalSelect(ctx) {
1452
1514
  };
1453
1515
  })).catch(() => [])).map((e) => e.text || e.title || e.aria).filter(Boolean);
1454
1516
  if (visibleTexts.length === 0) {
1455
- const isEmpty = await page.locator("[class*='empty'], [class*='no-data'], [role='status'], [class*='item-empty']").first().isVisible().catch(() => false);
1517
+ const isEmpty = targetDropdown !== null ? await targetDropdown.locator("[class*='empty'], [class*='no-data'], [role='status'], [class*='item-empty']").first().isVisible().catch(() => false) : false;
1456
1518
  await actualTarget.click({ timeout: 1e3 }).catch(() => {});
1457
1519
  if (isEmpty) throw new Error(`下拉浮层当前无选项(数据为空态/无可选数据): ${label}`);
1458
1520
  throw new Error(`点击 ${label} 后未出现下拉选项(选项列表为空态)`);
@@ -1761,6 +1823,7 @@ const scanInteractive = injectWalker((payload, w) => {
1761
1823
  }
1762
1824
  const hit = payload.exact ? legacyHit : legacyHit || tokens.length > 0 && tokenHit;
1763
1825
  if (legacyHit && name !== "" && name.toLowerCase() === q) score += 50;
1826
+ if (payload.preferredRoles && payload.preferredRoles.includes(role)) score += 40;
1764
1827
  scanned += 1;
1765
1828
  if (name !== "" && candidates.length < 50) candidates.push({
1766
1829
  role,
@@ -1857,7 +1920,8 @@ async function resolveSemanticTarget(page, what, opts = {}) {
1857
1920
  exact: opts.exact === true,
1858
1921
  scope: typeof opts.scope === "string" && opts.scope !== "" ? opts.scope : "",
1859
1922
  limit: Math.min(Math.max(opts.limit ?? 5, 1), 20),
1860
- dialogSelector: ACTIVE_DIALOG_SELECTORS
1923
+ dialogSelector: ACTIVE_DIALOG_SELECTORS,
1924
+ preferredRoles: opts.preferredRoles
1861
1925
  });
1862
1926
  } catch {
1863
1927
  return {
@@ -2034,7 +2098,7 @@ function nameSimilar(a, b) {
2034
2098
  * 自动识别 selector 或 ref;若 ref 标记位于子 Frame,自动绑定到对应子 Frame 上下文。
2035
2099
  * 具备路径防漂移校验与 getByRole 自愈降级能力。
2036
2100
  */
2037
- async function resolveTargetLocator(page, args) {
2101
+ async function resolveTargetLocator(page, args, opts = {}) {
2038
2102
  {
2039
2103
  const present = [];
2040
2104
  if (args.ref !== void 0 && args.ref !== "") present.push("ref");
@@ -2224,7 +2288,10 @@ async function resolveTargetLocator(page, args) {
2224
2288
  }
2225
2289
  const what = args.what;
2226
2290
  if (typeof what === "string" && what.trim() !== "" && args.selector === void 0 && args.ref === void 0 && args.mark === void 0 && rowPattern === void 0) {
2227
- const res = await resolveSemanticTarget(page, what, { scope: typeof args.scope === "string" && args.scope !== "" ? args.scope : void 0 });
2291
+ const res = await resolveSemanticTarget(page, what, {
2292
+ scope: typeof args.scope === "string" && args.scope !== "" ? args.scope : void 0,
2293
+ preferredRoles: opts.preferredRoles
2294
+ });
2228
2295
  if (!("candidates" in res)) return res;
2229
2296
  for (const role of [
2230
2297
  "button",
@@ -2466,24 +2533,43 @@ var ScreencastManager = class {
2466
2533
  cdp = null;
2467
2534
  currentPage = null;
2468
2535
  listeners = /* @__PURE__ */ new Set();
2469
- isStarting = false;
2470
2536
  lastFrame = null;
2537
+ /** 期望绑定的页面(syncTo/subscribe 写入);null 表示停流。 */
2538
+ desiredPage = null;
2539
+ /** ensure 单飞标记:在途收敛循环会在每个 await 后重读 desiredPage,吞并并发 syncTo。 */
2540
+ switching = false;
2471
2541
  /**
2472
2542
  * 注册帧监听器。返回注销函数。
2473
- * 当第一个监听器加入时自动激活 CDP Screencast;
2474
- * 当所有监听器离开时自动注销并停止 CDP,实现零空载开销。
2543
+ * page 为订阅时的初始目标页(由调用方解析)。首个订阅者加入时激活 CDP
2544
+ * Screencast;全部离开时注销并停止 CDP,保持零空载开销。
2475
2545
  */
2476
- subscribe(listener) {
2546
+ subscribe(listener, page) {
2477
2547
  this.listeners.add(listener);
2548
+ if (page !== void 0) this.desiredPage = page;
2478
2549
  if (this.lastFrame) try {
2479
2550
  listener(this.lastFrame);
2480
2551
  } catch {}
2481
- if (this.listeners.size === 1) this.start();
2552
+ if (this.listeners.size === 1) this.ensure();
2482
2553
  return () => {
2483
2554
  this.listeners.delete(listener);
2484
- if (this.listeners.size === 0) this.stop();
2555
+ if (this.listeners.size === 0) {
2556
+ this.desiredPage = null;
2557
+ this.ensure();
2558
+ }
2485
2559
  };
2486
2560
  }
2561
+ /**
2562
+ * 通知目标页变化(换页重绑 / 置 null 停流)。无订阅者时仅记录意图,
2563
+ * 不建 CDP 会话;快速连续换页由收敛循环合并,最终绑定最后一页。
2564
+ */
2565
+ async syncTo(page) {
2566
+ this.desiredPage = page;
2567
+ await this.ensure();
2568
+ }
2569
+ /** 停止推流并释放 CDP(closeBrowser 等收尾路径)。 */
2570
+ async stop() {
2571
+ await this.syncTo(null);
2572
+ }
2487
2573
  get subscriberCount() {
2488
2574
  return this.listeners.size;
2489
2575
  }
@@ -2491,26 +2577,54 @@ var ScreencastManager = class {
2491
2577
  return this.lastFrame;
2492
2578
  }
2493
2579
  /**
2494
- * 启动 CDP Screencast
2580
+ * 收敛循环:让 (cdp, currentPage) 与 (desiredPage, listeners) 达成一致。
2581
+ * 每轮重读 desiredPage —— await 期间到来的新 syncTo 在下一轮被吞并,
2582
+ * 不会因启动中而丢掉换页请求。
2495
2583
  */
2496
- async start(targetPage) {
2497
- if (this.cdp !== null || this.isStarting) return true;
2498
- const page = targetPage ?? activePage();
2499
- if (!page || page.isClosed()) return false;
2500
- this.isStarting = true;
2584
+ async ensure() {
2585
+ if (this.switching) return;
2586
+ this.switching = true;
2501
2587
  try {
2502
- this.currentPage = page;
2503
- page.once("close", () => {
2504
- if (this.currentPage === page) this.stop();
2505
- });
2506
- const context = page.context();
2507
- if (typeof context.newCDPSession !== "function") {
2508
- this.isStarting = false;
2509
- return false;
2588
+ for (let round = 0; round < 16; round++) {
2589
+ const want = this.desiredPage;
2590
+ if (this.listeners.size === 0 || want === null || want.isClosed()) {
2591
+ if (this.cdp === null) return;
2592
+ await this.teardown();
2593
+ if (this.desiredPage === want) return;
2594
+ continue;
2595
+ }
2596
+ if (this.currentPage === want && this.cdp !== null) return;
2597
+ await this.teardown();
2598
+ if (this.desiredPage !== want) continue;
2599
+ if (!await this.attach(want)) return;
2600
+ if (this.desiredPage !== want) continue;
2601
+ return;
2510
2602
  }
2603
+ } finally {
2604
+ this.switching = false;
2605
+ }
2606
+ }
2607
+ /** 停掉当前 CDP 会话并清空绑定;lastFrame 一并清空,防新订阅者回放到旧页帧。 */
2608
+ async teardown() {
2609
+ const cdp = this.cdp;
2610
+ this.cdp = null;
2611
+ this.currentPage = null;
2612
+ this.lastFrame = null;
2613
+ if (cdp !== null) try {
2614
+ await cdp.send("Page.stopScreencast").catch(() => {});
2615
+ await cdp.detach().catch(() => {});
2616
+ } catch {}
2617
+ }
2618
+ /** 为页面建立 CDP Screencast 会话;失败(非 Chromium 等)静默降级返回 false。 */
2619
+ async attach(page) {
2620
+ try {
2621
+ const context = page.context();
2622
+ if (typeof context.newCDPSession !== "function") return false;
2511
2623
  const cdp = await context.newCDPSession(page);
2512
2624
  this.cdp = cdp;
2625
+ this.currentPage = page;
2513
2626
  cdp.on("Page.screencastFrame", async (payload) => {
2627
+ if (this.cdp !== cdp) return;
2514
2628
  try {
2515
2629
  const frame = {
2516
2630
  buffer: Buffer.from(payload.data, "base64"),
@@ -2525,34 +2639,29 @@ var ScreencastManager = class {
2525
2639
  await cdp.send("Page.screencastFrameAck", { sessionId: payload.sessionId }).catch(() => {});
2526
2640
  } catch {}
2527
2641
  });
2528
- await cdp.send("Page.startScreencast", {
2529
- format: "jpeg",
2530
- quality: 60,
2531
- maxWidth: 1280,
2532
- maxHeight: 800,
2533
- everyNthFrame: 2
2642
+ try {
2643
+ await cdp.send("Page.startScreencast", {
2644
+ format: "jpeg",
2645
+ quality: 60,
2646
+ maxWidth: 1280,
2647
+ maxHeight: 800,
2648
+ everyNthFrame: 1
2649
+ });
2650
+ } catch (err) {
2651
+ if (this.cdp === cdp) await this.teardown();
2652
+ throw err;
2653
+ }
2654
+ page.once("close", () => {
2655
+ if (this.currentPage === page) {
2656
+ this.desiredPage = null;
2657
+ this.ensure();
2658
+ }
2534
2659
  });
2535
2660
  return true;
2536
2661
  } catch {
2537
- await this.stop();
2538
2662
  return false;
2539
- } finally {
2540
- this.isStarting = false;
2541
2663
  }
2542
2664
  }
2543
- /**
2544
- * 停止 CDP Screencast,静默释放资源
2545
- */
2546
- async stop() {
2547
- const cdp = this.cdp;
2548
- this.cdp = null;
2549
- this.currentPage = null;
2550
- this.isStarting = false;
2551
- if (cdp !== null) try {
2552
- await cdp.send("Page.stopScreencast").catch(() => {});
2553
- await cdp.detach().catch(() => {});
2554
- } catch {}
2555
- }
2556
2665
  };
2557
2666
  const screencastManager = new ScreencastManager();
2558
2667
  //#endregion
@@ -2897,6 +3006,12 @@ function pruneClosedPages(state) {
2897
3006
  if (state.activeId === id) state.activeId = null;
2898
3007
  }
2899
3008
  }
3009
+ /** 实时画面流跟随活动页(推模型:session 层在 activeId 变化点主动通知,
3010
+ * screencast 模块不反查本 facade,避免循环依赖)。 */
3011
+ function syncScreencast(state) {
3012
+ if (state !== stateOf()) return;
3013
+ screencastManager.syncTo(state.pages.get(state.activeId ?? -1) ?? null);
3014
+ }
2900
3015
  /** 把一个已 attach 的页面注册进 profile 标签表并激活,返回分配的 tab id。
2901
3016
  * 唯一注册路径:同时挂 close 监听幂等摘除外部关闭的页(否则僵尸句柄会永久占用
2902
3017
  * activeId,所有工具连报 "Target page...closed" 而 tabs list 仍显示存活)。 */
@@ -2905,11 +3020,13 @@ function registerTabPage(state, page) {
2905
3020
  state.pages.set(id, page);
2906
3021
  state.activeId = id;
2907
3022
  (state.tabMeta ??= /* @__PURE__ */ new Map()).set(id, { createdAt: Date.now() });
3023
+ syncScreencast(state);
2908
3024
  page.on("close", () => {
2909
3025
  if (!state.pages.has(id)) return;
2910
3026
  state.pages.delete(id);
2911
3027
  state.tabMeta?.delete(id);
2912
3028
  if (state.activeId === id) state.activeId = null;
3029
+ syncScreencast(state);
2913
3030
  });
2914
3031
  return id;
2915
3032
  }
@@ -3020,6 +3137,8 @@ function setLastFormFields(fields) {
3020
3137
  /** 切换当前 profile。 */
3021
3138
  function setCurrentProfile(name) {
3022
3139
  currentProfile = name;
3140
+ const state = stateOf();
3141
+ screencastManager.syncTo(state.pages.get(state.activeId ?? -1) ?? null);
3023
3142
  }
3024
3143
  //#endregion
3025
3144
  //#region lib/types/browser/tools/navigation.js
@@ -4261,7 +4380,11 @@ async function clickTool(config, args, exec) {
4261
4380
  }
4262
4381
  async function typeTool(config, args) {
4263
4382
  const page = await getPage(config);
4264
- const targetRes = await resolveTargetLocator(page, args);
4383
+ const targetRes = await resolveTargetLocator(page, args, { preferredRoles: [
4384
+ "textbox",
4385
+ "searchbox",
4386
+ "spinbutton"
4387
+ ] });
4265
4388
  const target = targetRes.locator;
4266
4389
  const strictErr = strictReject(args, targetRes);
4267
4390
  if (strictErr !== null) throw strictErr;
@@ -4352,7 +4475,11 @@ async function pressTool(config, args) {
4352
4475
  */
4353
4476
  async function selectTool(config, args) {
4354
4477
  const page = await getPage(config);
4355
- const targetRes = await resolveTargetLocator(page, args);
4478
+ const targetRes = await resolveTargetLocator(page, args, { preferredRoles: [
4479
+ "combobox",
4480
+ "select",
4481
+ "listbox"
4482
+ ] });
4356
4483
  const target = targetRes.locator;
4357
4484
  const strictErr = strictReject(args, targetRes);
4358
4485
  if (strictErr !== null) throw strictErr;
@@ -5308,25 +5435,187 @@ function previewHighlight(page, selector, label) {
5308
5435
  }).catch(() => {});
5309
5436
  }
5310
5437
  /**
5438
+ * 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
5439
+ */
5440
+ const AUTH_URL_PATTERN = /login|signin|sign-in|oauth|passport|sso|cas|challenge|captcha|verify|auth\b/i;
5441
+ /**
5442
+ * 等待人工登录底层执行(独立导出供测试注入 fake page)。
5443
+ * 协同机制:
5444
+ * 1. 挂起等待期间置入 instruction 至实时画面面板(setPauseInstruction),结束后统一清理;
5445
+ * 2. 存活标签感知:感知 context 内所有存活页面,原标签关闭时自动迁移至存活页,新标签出现时通知外层注册;
5446
+ * 3. 稳态判定:
5447
+ * - 显式 successSelector / successUrl 优先;
5448
+ * - 缺省时,检测 URL 离开登录页(非 AUTH_URL_PATTERN)、登录密码框消除、或 SPA 内容签名稳态变化,防范跳板页误判与 SPA 弹窗漏判。
5449
+ */
5450
+ async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
5451
+ const instruction = typeof args.instruction === "string" && args.instruction.trim() ? args.instruction.trim() : "请在浏览器窗口中完成登录";
5452
+ const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 12e4) || 12e4, 1e3), 6e5);
5453
+ const pollIntervalMs = Math.min(Math.max(Number(args.pollIntervalMs ?? 2e3) || 2e3, 100), 5e3);
5454
+ let currentPage = initialPage;
5455
+ let startUrl = "";
5456
+ try {
5457
+ startUrl = initialPage.url();
5458
+ } catch {}
5459
+ const checkHasPassword = async (p) => {
5460
+ try {
5461
+ return await p.evaluate(() => !!document.querySelector("input[type=\"password\"]"));
5462
+ } catch {
5463
+ return false;
5464
+ }
5465
+ };
5466
+ const getPageSig = async (p) => {
5467
+ try {
5468
+ return await p.evaluate(() => {
5469
+ const body = document.body;
5470
+ return `${location.href}|${body ? body.innerText.length : 0}|${document.querySelectorAll("input,button,a").length}`;
5471
+ });
5472
+ } catch {
5473
+ try {
5474
+ return p.url();
5475
+ } catch {
5476
+ return "";
5477
+ }
5478
+ }
5479
+ };
5480
+ const startHasPassword = await checkHasPassword(initialPage);
5481
+ const startSig = args.successSelector || args.successUrl ? "" : await getPageSig(initialPage);
5482
+ const startedAt = Date.now();
5483
+ let loggedIn = false;
5484
+ let detail = `等待登录超时(${timeoutMs}ms)`;
5485
+ setPauseInstruction(instruction);
5486
+ try {
5487
+ const deadline = startedAt + timeoutMs;
5488
+ while (Date.now() < deadline) {
5489
+ let candidatePages = [];
5490
+ try {
5491
+ const context = typeof currentPage.context === "function" ? currentPage.context() : null;
5492
+ if (context && typeof context.pages === "function") candidatePages = context.pages().filter((p) => {
5493
+ try {
5494
+ return !p.isClosed();
5495
+ } catch {
5496
+ return false;
5497
+ }
5498
+ });
5499
+ } catch {}
5500
+ let isCurrentClosed = false;
5501
+ try {
5502
+ isCurrentClosed = currentPage.isClosed();
5503
+ } catch {
5504
+ isCurrentClosed = true;
5505
+ }
5506
+ if (candidatePages.length > 0) {
5507
+ for (const p of candidatePages) if (p !== currentPage && onNewPageDetected) onNewPageDetected(p);
5508
+ if (isCurrentClosed) {
5509
+ currentPage = candidatePages[candidatePages.length - 1];
5510
+ isCurrentClosed = false;
5511
+ }
5512
+ }
5513
+ try {
5514
+ if (args.successSelector) {
5515
+ const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
5516
+ for (const p of targets) try {
5517
+ if (await p.locator(String(args.successSelector)).first().isVisible()) {
5518
+ loggedIn = true;
5519
+ currentPage = p;
5520
+ detail = `命中 successSelector: ${args.successSelector}`;
5521
+ break;
5522
+ }
5523
+ } catch {}
5524
+ } else if (args.successUrl) {
5525
+ const re = new RegExp(String(args.successUrl));
5526
+ const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
5527
+ for (const p of targets) try {
5528
+ const u = p.url();
5529
+ if (re.test(u)) {
5530
+ loggedIn = true;
5531
+ currentPage = p;
5532
+ detail = `命中 successUrl: ${args.successUrl}`;
5533
+ break;
5534
+ }
5535
+ } catch {}
5536
+ } else {
5537
+ const target = !isCurrentClosed ? currentPage : candidatePages[0];
5538
+ if (target) {
5539
+ let curUrl = "";
5540
+ try {
5541
+ curUrl = target.url();
5542
+ } catch {}
5543
+ if (curUrl && curUrl !== "about:blank") {
5544
+ if (curUrl !== startUrl) {
5545
+ if (!AUTH_URL_PATTERN.test(curUrl)) {
5546
+ loggedIn = true;
5547
+ currentPage = target;
5548
+ detail = `URL 离开登录页跳转至 ${curUrl}`;
5549
+ } else {
5550
+ const hasPwd = await checkHasPassword(target);
5551
+ if (startHasPassword && !hasPwd) {
5552
+ loggedIn = true;
5553
+ currentPage = target;
5554
+ detail = `登录密码输入框已消除,页面跳转至 ${curUrl}`;
5555
+ }
5556
+ }
5557
+ } else {
5558
+ const hasPwd = await checkHasPassword(target);
5559
+ if (startHasPassword && !hasPwd) {
5560
+ loggedIn = true;
5561
+ currentPage = target;
5562
+ detail = "登录密码输入框已消除(SPA 登录完成)";
5563
+ } else if (await getPageSig(target) !== startSig && (!startHasPassword || !hasPwd)) {
5564
+ loggedIn = true;
5565
+ currentPage = target;
5566
+ detail = "页面内容签名变更(SPA 登录完成)";
5567
+ }
5568
+ }
5569
+ }
5570
+ }
5571
+ }
5572
+ } catch (e) {
5573
+ detail = `检查登录状态出错: ${e instanceof Error ? e.message : String(e)}`;
5574
+ }
5575
+ if (loggedIn) break;
5576
+ try {
5577
+ await currentPage.waitForTimeout(pollIntervalMs);
5578
+ } catch {
5579
+ await sleep$1(pollIntervalMs);
5580
+ }
5581
+ }
5582
+ } finally {
5583
+ clearPauseInstruction();
5584
+ }
5585
+ return {
5586
+ loggedIn,
5587
+ waitedMs: Date.now() - startedAt,
5588
+ detail,
5589
+ targetPage: currentPage
5590
+ };
5591
+ }
5592
+ /**
5311
5593
  * 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
5312
- * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。基于通用条件引擎:
5313
- * successSelector / successUrl 指定完成条件,两者都不给则以 URL 跳转视为完成。
5314
- * 超时返回 loggedIn:false(不抛错)。
5594
+ * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。支持 instruction 提示语
5595
+ * 与右下角实时画面面板联动显示横幅,支持多标签感知与 SPA 签名自适应判定。
5596
+ * 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
5315
5597
  */
5316
5598
  async function waitForLoginTool(config, args) {
5317
5599
  const page = await getPage(config);
5318
- const startUrl = page.url();
5319
- let condition;
5320
- if (args.successSelector) condition = {
5321
- selector: String(args.successSelector),
5322
- state: "visible"
5323
- };
5324
- else if (args.successUrl) condition = { url: String(args.successUrl) };
5325
- else condition = { eval: `location.href !== ${JSON.stringify(startUrl)} && location.href !== "about:blank"` };
5326
- const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 12e4) || 12e4, 5e3), 6e5);
5600
+ const state = stateOf();
5601
+ const result = await runWaitForLoginOnPage(page, args, (newPage) => {
5602
+ if (![...state.pages.values()].includes(newPage)) {
5603
+ attachPage(newPage, profileConfig(config, currentProfile));
5604
+ registerTabPage(state, newPage);
5605
+ }
5606
+ });
5607
+ if (result.targetPage !== page) {
5608
+ for (const [id, p] of state.pages) if (p === result.targetPage) {
5609
+ state.activeId = id;
5610
+ break;
5611
+ }
5612
+ }
5613
+ const identity = await pageIdentity(result.targetPage);
5327
5614
  return {
5328
- loggedIn: (await pollCondition(page, condition, timeoutMs, 2e3)).ok,
5329
- ...await pageIdentity(page)
5615
+ loggedIn: result.loggedIn,
5616
+ waitedMs: result.waitedMs,
5617
+ detail: result.detail,
5618
+ ...identity
5330
5619
  };
5331
5620
  }
5332
5621
  /**
@@ -8373,10 +8662,14 @@ function getCollaborationTools(config, timeout, stepImpls) {
8373
8662
  return [
8374
8663
  definePlainTool({
8375
8664
  name: "browser_wait_for_login",
8376
- description: "等待人工登录完成。agent 在登录页调用本工具后,提示用户在浏览器窗口中完成登录(有头模式 headless:false 时用户可直接操作;配 userDataDir 登录态会持久化)。成功条件三选一:successSelector(登录后出现的元素,如 '#avatar')或 successUrl(登录后 URL 正则,如 'https://\\\\.app\\\\.example\\\\.com')都不给则 URL 发生变化即视为完成。轮询间隔 2 秒,超时返回 loggedIn:false。",
8665
+ description: "等待人工登录完成。agent 在登录页调用本工具后,提示用户在浏览器窗口中完成登录(有头模式 headless:false 时用户可直接操作;配 userDataDir 登录态会持久化)。支持 instruction 提示语(将显示在右下角实时画面面板);成功条件:优先取 successSelector(登录后出现的元素,如 '#avatar')或 successUrl(登录后 URL 正则);两者未指定时,结合 URL 变化、登录特征消除(密码框等)与页面签名稳态综合判定。轮询间隔 2 秒,超时返回 loggedIn:false(附 waitedMs 与 detail)。",
8377
8666
  parameters: {
8378
8667
  type: "object",
8379
8668
  properties: {
8669
+ instruction: {
8670
+ type: "string",
8671
+ description: "可选:给用户看的提示说明(默认「请在浏览器窗口中完成登录」)。"
8672
+ },
8380
8673
  timeoutMs: {
8381
8674
  type: "integer",
8382
8675
  description: "等待上限(毫秒,默认 120000,上限 600000)。"
@@ -8397,11 +8690,15 @@ function getCollaborationTools(config, timeout, stepImpls) {
8397
8690
  additionalProperties: false,
8398
8691
  properties: {
8399
8692
  loggedIn: { type: "boolean" },
8693
+ waitedMs: { type: "integer" },
8694
+ detail: { type: "string" },
8400
8695
  url: { type: "string" },
8401
8696
  title: { type: "string" }
8402
8697
  },
8403
8698
  required: [
8404
8699
  "loggedIn",
8700
+ "waitedMs",
8701
+ "detail",
8405
8702
  "url",
8406
8703
  "title"
8407
8704
  ]
@@ -9414,7 +9711,7 @@ function registerBrowserApi(ctx, config, base) {
9414
9711
  timestamp: frame.timestamp
9415
9712
  })}\n\n`);
9416
9713
  } catch {}
9417
- });
9714
+ }, page);
9418
9715
  req.on("close", () => {
9419
9716
  unsubscribe();
9420
9717
  });
@@ -9432,7 +9729,7 @@ function registerBrowserApi(ctx, config, base) {
9432
9729
  res.write(frame.buffer);
9433
9730
  res.write("\r\n");
9434
9731
  } catch {}
9435
- });
9732
+ }, page);
9436
9733
  req.on("close", () => {
9437
9734
  unsubscribe();
9438
9735
  });
@@ -9,29 +9,40 @@ export interface ScreencastFrame {
9
9
  timestamp: number;
10
10
  }
11
11
  export type FrameListener = (frame: ScreencastFrame) => void;
12
- declare class ScreencastManager {
12
+ export declare class ScreencastManager {
13
13
  private cdp;
14
14
  private currentPage;
15
15
  private listeners;
16
- private isStarting;
17
16
  private lastFrame;
17
+ /** 期望绑定的页面(syncTo/subscribe 写入);null 表示停流。 */
18
+ private desiredPage;
19
+ /** ensure 单飞标记:在途收敛循环会在每个 await 后重读 desiredPage,吞并并发 syncTo。 */
20
+ private switching;
18
21
  /**
19
22
  * 注册帧监听器。返回注销函数。
20
- * 当第一个监听器加入时自动激活 CDP Screencast;
21
- * 当所有监听器离开时自动注销并停止 CDP,实现零空载开销。
23
+ * page 为订阅时的初始目标页(由调用方解析)。首个订阅者加入时激活 CDP
24
+ * Screencast;全部离开时注销并停止 CDP,保持零空载开销。
22
25
  */
23
- subscribe(listener: FrameListener): () => void;
24
- get subscriberCount(): number;
25
- getLastFrame(): ScreencastFrame | null;
26
+ subscribe(listener: FrameListener, page?: Page | null): () => void;
26
27
  /**
27
- * 启动 CDP Screencast
28
+ * 通知目标页变化(换页重绑 / 置 null 停流)。无订阅者时仅记录意图,
29
+ * 不建 CDP 会话;快速连续换页由收敛循环合并,最终绑定最后一页。
28
30
  */
29
- start(targetPage?: Page | null): Promise<boolean>;
31
+ syncTo(page: Page | null): Promise<void>;
32
+ /** 停止推流并释放 CDP(closeBrowser 等收尾路径)。 */
33
+ stop(): Promise<void>;
34
+ get subscriberCount(): number;
35
+ getLastFrame(): ScreencastFrame | null;
30
36
  /**
31
- * 停止 CDP Screencast,静默释放资源
37
+ * 收敛循环:让 (cdp, currentPage) 与 (desiredPage, listeners) 达成一致。
38
+ * 每轮重读 desiredPage —— await 期间到来的新 syncTo 在下一轮被吞并,
39
+ * 不会因启动中而丢掉换页请求。
32
40
  */
33
- stop(): Promise<void>;
41
+ private ensure;
42
+ /** 停掉当前 CDP 会话并清空绑定;lastFrame 一并清空,防新订阅者回放到旧页帧。 */
43
+ private teardown;
44
+ /** 为页面建立 CDP Screencast 会话;失败(非 Chromium 等)静默降级返回 false。 */
45
+ private attach;
34
46
  }
35
47
  export declare const screencastManager: ScreencastManager;
36
- export {};
37
48
  //# sourceMappingURL=screencast.d.ts.map
@@ -35,6 +35,7 @@ export declare const scanInteractive: (payload: {
35
35
  scope: string;
36
36
  limit: number;
37
37
  dialogSelector?: string;
38
+ preferredRoles?: string[];
38
39
  }) => {
39
40
  matches: SemanticMatch[];
40
41
  candidates: {
@@ -65,6 +66,7 @@ export declare function resolveSemanticTarget(page: Page, what: string, opts?: {
65
66
  scope?: string;
66
67
  limit?: number;
67
68
  maxRefs?: number;
69
+ preferredRoles?: string[];
68
70
  }): Promise<TargetResolution | SemanticMiss>;
69
71
  /**
70
72
  * 全量可交互元素扫描(SPA ref 重锚定的数据源):返回页面(或 scope 内)全部
@@ -10,7 +10,9 @@ export declare function buildFlexibleTextPattern(text: string): RegExp;
10
10
  * 自动识别 selector 或 ref;若 ref 标记位于子 Frame,自动绑定到对应子 Frame 上下文。
11
11
  * 具备路径防漂移校验与 getByRole 自愈降级能力。
12
12
  */
13
- export declare function resolveTargetLocator(page: Page, args: Record<string, any>): Promise<TargetResolution>;
13
+ export declare function resolveTargetLocator(page: Page, args: Record<string, any>, opts?: {
14
+ preferredRoles?: string[];
15
+ }): Promise<TargetResolution>;
14
16
  /** 兼容旧版签名的目标定位快捷方式 */
15
17
  export declare function targetLocator(page: Page, args: Record<string, any>): Promise<Locator>;
16
18
  //# sourceMappingURL=target.d.ts.map
@@ -5,16 +5,38 @@ import type { BrowserConfig } from "../types.js";
5
5
  * ~1.5 秒。异步不阻塞(fire-and-forget,任何失败静默),不影响操作时序。
6
6
  */
7
7
  export declare function previewHighlight(page: Page | null, selector: string, label: string): void;
8
+ /**
9
+ * 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
10
+ */
11
+ export declare const AUTH_URL_PATTERN: RegExp;
12
+ export interface WaitForLoginResult {
13
+ loggedIn: boolean;
14
+ waitedMs: number;
15
+ detail: string;
16
+ targetPage: Page;
17
+ }
18
+ /**
19
+ * 等待人工登录底层执行(独立导出供测试注入 fake page)。
20
+ * 协同机制:
21
+ * 1. 挂起等待期间置入 instruction 至实时画面面板(setPauseInstruction),结束后统一清理;
22
+ * 2. 存活标签感知:感知 context 内所有存活页面,原标签关闭时自动迁移至存活页,新标签出现时通知外层注册;
23
+ * 3. 稳态判定:
24
+ * - 显式 successSelector / successUrl 优先;
25
+ * - 缺省时,检测 URL 离开登录页(非 AUTH_URL_PATTERN)、登录密码框消除、或 SPA 内容签名稳态变化,防范跳板页误判与 SPA 弹窗漏判。
26
+ */
27
+ export declare function runWaitForLoginOnPage(initialPage: Page, args: Record<string, any>, onNewPageDetected?: (page: Page) => void): Promise<WaitForLoginResult>;
8
28
  /**
9
29
  * 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
10
- * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。基于通用条件引擎:
11
- * successSelector / successUrl 指定完成条件,两者都不给则以 URL 跳转视为完成。
12
- * 超时返回 loggedIn:false(不抛错)。
30
+ * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。支持 instruction 提示语
31
+ * 与右下角实时画面面板联动显示横幅,支持多标签感知与 SPA 签名自适应判定。
32
+ * 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
13
33
  */
14
34
  export declare function waitForLoginTool(config: BrowserConfig, args: Record<string, any>): Promise<{
15
35
  url: string;
16
36
  title: string;
17
37
  loggedIn: boolean;
38
+ waitedMs: number;
39
+ detail: string;
18
40
  }>;
19
41
  /**
20
42
  * 人工接管等待(通用版 wait_for_login):agent 把控制权交还给用户(验证码 /
@@ -0,0 +1,21 @@
1
+ import { type CSSProperties, type MouseEvent } from "react";
2
+ interface LiveImageProps {
3
+ /** MJPEG 流地址(/stream);缺省时退化为纯截图轮询 */
4
+ streamSrc?: string;
5
+ /** 轮询截图地址(/screenshot?v=…,v 随状态轮询变化) */
6
+ shotSrc: string;
7
+ alt?: string;
8
+ title?: string;
9
+ style?: CSSProperties;
10
+ onClick?: (e: MouseEvent<HTMLImageElement>) => void;
11
+ }
12
+ export declare function LiveImage({ streamSrc, shotSrc, alt, title, style, onClick }: LiveImageProps): import("react").DetailedReactHTMLElement<{
13
+ src: string;
14
+ onError: () => void;
15
+ alt: string | undefined;
16
+ title: string | undefined;
17
+ onClick: ((e: MouseEvent<HTMLImageElement>) => void) | undefined;
18
+ style: CSSProperties | undefined;
19
+ }, HTMLElement>;
20
+ export {};
21
+ //# sourceMappingURL=LiveImage.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-view",
3
- "version": "0.7.2",
3
+ "version": "0.7.5",
4
4
  "description": "DSH 插件:共享 Playwright 浏览器自动化(38 个 browser_* 工具,含 what 意图语义定位(一次调用替代快照→查找→点击)、SoM 标徽看图点击与零命中截图兜底、SPA ref 软失效重锚定、open Shadow DOM 组合树穿透、跨 Frame/IFrame 定位、视口感知动态快照、批量表单智能填充、点击效果验证与定位失败遥测、Playwright 规范代码导出、批处理、视觉回归、选择器自愈、增量快照)+ 人机协作(面板点选教学 browser_pick_element、人工接管等待、敏感操作审批硬拦截、人机混合录制、面板一键重跑)+ CDP 流式推流、拟人化轨迹与高斯击键、全景时光机、企业富控件矩阵。仅依赖 playwright-core。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -36,6 +36,7 @@
36
36
  "test:shadow": "tsx scripts/test-dev-server-shadow.ts",
37
37
  "test:span": "tsx scripts/test-dev-server-clickable-span.ts",
38
38
  "test:pick": "tsx scripts/test-panel-pick.ts",
39
+ "test:stream": "tsx scripts/test-panel-stream.ts",
39
40
  "dev:server": "tsx scripts/dev-server.ts",
40
41
  "verify": "npm run typecheck && npm run lint && npm run test && npm run build",
41
42
  "prepublishOnly": "npm run verify"