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 +1 -1
- package/lib/client.js +1 -1
- package/lib/index.js +370 -73
- package/lib/types/browser/session/screencast.d.ts +23 -12
- package/lib/types/browser/session/semantic.d.ts +2 -0
- package/lib/types/browser/session/target.d.ts +3 -1
- package/lib/types/browser/tools/collaboration.d.ts +25 -3
- package/lib/types/client/components/LiveImage.d.ts +21 -0
- package/package.json +2 -1
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 界面右下角悬浮面板:浏览器状态 + 实时缩略图,点开大屏模态框(
|
|
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
|
-
|
|
1380
|
-
|
|
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
|
|
1439
|
-
const
|
|
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
|
|
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, {
|
|
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
|
-
*
|
|
2474
|
-
*
|
|
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.
|
|
2552
|
+
if (this.listeners.size === 1) this.ensure();
|
|
2482
2553
|
return () => {
|
|
2483
2554
|
this.listeners.delete(listener);
|
|
2484
|
-
if (this.listeners.size === 0)
|
|
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
|
-
*
|
|
2580
|
+
* 收敛循环:让 (cdp, currentPage) 与 (desiredPage, listeners) 达成一致。
|
|
2581
|
+
* 每轮重读 desiredPage —— await 期间到来的新 syncTo 在下一轮被吞并,
|
|
2582
|
+
* 不会因启动中而丢掉换页请求。
|
|
2495
2583
|
*/
|
|
2496
|
-
async
|
|
2497
|
-
if (this.
|
|
2498
|
-
|
|
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
|
-
|
|
2503
|
-
|
|
2504
|
-
if (this.
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
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
|
-
*
|
|
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
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
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:
|
|
5329
|
-
|
|
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 登录态会持久化)
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
28
|
+
* 通知目标页变化(换页重绑 / 置 null 停流)。无订阅者时仅记录意图,
|
|
29
|
+
* 不建 CDP 会话;快速连续换页由收敛循环合并,最终绑定最后一页。
|
|
28
30
|
*/
|
|
29
|
-
|
|
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
|
-
*
|
|
37
|
+
* 收敛循环:让 (cdp, currentPage) 与 (desiredPage, listeners) 达成一致。
|
|
38
|
+
* 每轮重读 desiredPage —— await 期间到来的新 syncTo 在下一轮被吞并,
|
|
39
|
+
* 不会因启动中而丢掉换页请求。
|
|
32
40
|
*/
|
|
33
|
-
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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"
|