dsh-browser-view 0.8.3 → 0.9.0
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 +3 -0
- package/cordis.patch.yml +1 -0
- package/lib/client.js +1 -1
- package/lib/index.js +46 -7
- package/lib/mcp.js +46 -7
- package/lib/types/browser/session.d.ts +15 -0
- package/lib/types/browser/types.d.ts +10 -0
- package/lib/types/client/index.d.ts +4 -2
- package/lib/types/client/navIcon.d.ts +7 -0
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -30,6 +30,9 @@ dsh plugin --profile web add dsh-browser-view
|
|
|
30
30
|
executablePath: "" # 显式指定浏览器可执行文件(优先于 channel)
|
|
31
31
|
headless: false # 默认有头(本机调试/人工登录);服务器场景可改 true
|
|
32
32
|
userDataDir: "" # 设置后登录态跨 DSH 重启保留
|
|
33
|
+
# viewport: 缺省 = Playwright 默认 1280×720 固定视口仿真(不随窗口变化,测试确定性最佳)
|
|
34
|
+
# viewport: window # 视口跟随浏览器窗口大小(拖动窗口页面实时重排)
|
|
35
|
+
# viewport: { width: 1920, height: 1080 } # 自定义固定视口
|
|
33
36
|
profiles: {} # { work: { userDataDir, channel, headless }, ... }
|
|
34
37
|
workspaceDir: "" # 截图/下载/上传的落盘基准目录(默认探测会话 cwd,多工作区/服务器场景可钉死)
|
|
35
38
|
screenshotDir: .dsh-browser/screenshots
|
package/cordis.patch.yml
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# executablePath 显式浏览器可执行文件(优先于 channel)
|
|
7
7
|
# headless 无头模式(默认 false = 有头窗口,适合本机调试/人工登录)
|
|
8
8
|
# userDataDir 登录态持久化目录(设置后 Cookie/localStorage 跨 DSH 重启保留)
|
|
9
|
+
# viewport 页面视口:'window'(跟随窗口大小)| { width, height }(固定);缺省 = 1280×720 仿真
|
|
9
10
|
# profiles 多配置文件:{ work: { userDataDir, channel, headless }, ... }
|
|
10
11
|
# screenshotDir 截图目录(相对工作区,默认 .dsh-browser/screenshots)
|
|
11
12
|
# downloadDir 下载目录(默认 .dsh-browser/downloads)
|
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){try{let t=await fetch(`${e}/login`,{cache:`no-store`});return t.ok?(await t.json())?.loginWait??null:null}catch{return null}}async function m(e,t){try{return(await fetch(`${e}/login`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t})})).ok}catch{return!1}}async function h(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 g(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 _(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 v(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 y(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 b(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,m]=(0,r.useState)(!1),[h,_]=(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),m(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:h,rerunLast:async()=>{let t=d;if(!t||p)return;let n=t.source.replace(/\(已从磁盘重载\)$/,``);if(!n||[`steps`,`recording`,`recent-log`].includes(n)){_(`该来源不是已保存录制,无法一键重跑`);return}_(`重跑中…`),m(!0);let r=await g(e,n,t.runId);r.ok?_(`重跑完成,报告稍后刷新`):(m(!1),_(r.error??`重跑失败`))}}}function x(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 S(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await p(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]),{loginWait:t,confirmLoginDone:async t=>{await m(e,t)&&n(null)}}}function C(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 w={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},T={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function E(){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 D(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function O({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 k({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 A({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)(O,{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)(k,{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`}},D(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 j({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)(k,{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`}},D(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function M({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 N({loginWait:e,onDone:t}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`260px`,padding:`10px 12px`,borderRadius:`10px`,border:`1px solid #16a34a`,background:`rgba(22, 163, 74, 0.12)`,color:`#15803d`,fontSize:`12px`,lineHeight:`1.5`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.18)`,pointerEvents:`auto`},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,fontWeight:600,wordBreak:`break-all`}},`等待人工登录`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`,opacity:.9}},e.instruction),(0,r.createElement)(`button`,{onClick:t,style:{width:`100%`,marginTop:`8px`,padding:`6px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`13px`,fontWeight:600}},`我已完成登录`))}function P(){let e=E(),t=e?T:w,[n,i]=(0,r.useState)(`/browser`),[o,c]=(0,r.useState)(!1),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[p,m]=(0,r.useState)([]);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&i(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{status:g,ops:_,shotVersion:v,lastRun:y,agentBusy:D,rerunMsg:O,rerunLast:k}=b(n),{pendingConfirm:P,resolveConfirm:F}=x(n),{loginWait:I,confirmLoginDone:L}=S(n),{pendingPick:R}=C(n);(0,r.useEffect)(()=>{if(!l)return;let e=!0,t=async()=>{let t=await s(n);e&&m(t)};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[l,n]);let z=`${n}/screenshot?v=${v}`,B=`${n}/stream`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},I&&!I.done?(0,r.createElement)(N,{loginWait:I,onDone:()=>{L(I.id)}}):null,(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)(A,{status:g,ops:_,lastRun:y,agentBusy:D,rerunMsg:O,pendingConfirm:P,pendingPick:R,shotSrc:z,streamSrc:B,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{P&&F(P.id,e)},onRerunLast:k}),l&&(0,r.createElement)(j,{status:g,history:p,shotSrc:z,streamSrc:B,dark:e,palette:t,onClose:()=>u(!1),pendingPick:R,onPick:(e,t,r,i)=>{R&&h(n,R.id,e,t,r,i)}}),d&&(0,r.createElement)(M,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function F(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(``),_(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 v(e,t).then(e=>{s(n=>({...n,[t]:e??`failed`}))}).catch(()=>s(e=>({...e,[t]:`failed`}))),{...n,[t]:`loading`}})},remove:async t=>{await y(e,t)&&(n(e=>e===null?e:e.filter(e=>e.name!==t)),s(e=>{let n={...e};return delete n[t],n}))}}}function I(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 L(e){let t=E(),n=t?T:w,i=I(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}=F(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 R=[`slots`];function z(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},L)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},P))}return n.apply=z,n.inject=R,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){try{let t=await fetch(`${e}/login`,{cache:`no-store`});return t.ok?(await t.json())?.loginWait??null:null}catch{return null}}async function m(e,t){try{return(await fetch(`${e}/login`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t})})).ok}catch{return!1}}async function h(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 g(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 _(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 v(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 y(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 b(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,m]=(0,r.useState)(!1),[h,_]=(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),m(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:h,rerunLast:async()=>{let t=d;if(!t||p)return;let n=t.source.replace(/\(已从磁盘重载\)$/,``);if(!n||[`steps`,`recording`,`recent-log`].includes(n)){_(`该来源不是已保存录制,无法一键重跑`);return}_(`重跑中…`),m(!0);let r=await g(e,n,t.runId);r.ok?_(`重跑完成,报告稍后刷新`):(m(!1),_(r.error??`重跑失败`))}}}function x(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 S(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await p(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]),{loginWait:t,confirmLoginDone:async t=>{await m(e,t)&&n(null)}}}function C(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 w={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},T={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function E(){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 D(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function O({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 k({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 A({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)(O,{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)(k,{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`}},D(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 j({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)(k,{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`}},D(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function M({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 N({loginWait:e,onDone:t}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`260px`,padding:`10px 12px`,borderRadius:`10px`,border:`1px solid #16a34a`,background:`rgba(22, 163, 74, 0.12)`,color:`#15803d`,fontSize:`12px`,lineHeight:`1.5`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.18)`,pointerEvents:`auto`},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,fontWeight:600,wordBreak:`break-all`}},`等待人工登录`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`,opacity:.9}},e.instruction),(0,r.createElement)(`button`,{onClick:t,style:{width:`100%`,marginTop:`8px`,padding:`6px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`13px`,fontWeight:600}},`我已完成登录`))}function P(){let e=E(),t=e?T:w,[n,i]=(0,r.useState)(`/browser`),[o,c]=(0,r.useState)(!1),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[p,m]=(0,r.useState)([]);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&i(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{status:g,ops:_,shotVersion:v,lastRun:y,agentBusy:D,rerunMsg:O,rerunLast:k}=b(n),{pendingConfirm:P,resolveConfirm:F}=x(n),{loginWait:I,confirmLoginDone:L}=S(n),{pendingPick:R}=C(n);(0,r.useEffect)(()=>{if(!l)return;let e=!0,t=async()=>{let t=await s(n);e&&m(t)};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[l,n]);let z=`${n}/screenshot?v=${v}`,B=`${n}/stream`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},I&&!I.done?(0,r.createElement)(N,{loginWait:I,onDone:()=>{L(I.id)}}):null,(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)(A,{status:g,ops:_,lastRun:y,agentBusy:D,rerunMsg:O,pendingConfirm:P,pendingPick:R,shotSrc:z,streamSrc:B,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{P&&F(P.id,e)},onRerunLast:k}),l&&(0,r.createElement)(j,{status:g,history:p,shotSrc:z,streamSrc:B,dark:e,palette:t,onClose:()=>u(!1),pendingPick:R,onPick:(e,t,r,i)=>{R&&h(n,R.id,e,t,r,i)}}),d&&(0,r.createElement)(M,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function F(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(``),_(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 v(e,t).then(e=>{s(n=>({...n,[t]:e??`failed`}))}).catch(()=>s(e=>({...e,[t]:`failed`}))),{...n,[t]:`loading`}})},remove:async t=>{await y(e,t)&&(n(e=>e===null?e:e.filter(e=>e.name!==t)),s(e=>{let n={...e};return delete n[t],n}))}}}function I(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 L(e){let t=E(),n=t?T:w,i=I(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}=F(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 R=[`浏览器`,`🌐 浏览器`],z=`data-dsh-browser-nav-icon`,B=`data-dsh-browser-nav-hidden`;function V(){if(typeof document>`u`)return;let e=document.querySelectorAll(`button[class*="navCell"]`),t=e.length>0?Array.from(e):Array.from(document.querySelectorAll(`[class*="navList"] button`));for(let e of t){let t=(e.querySelector(`[class*="navLabel"]`)?.textContent??e.textContent??``).trim();if(!R.some(e=>t===e)||e.querySelector(`[${z}]`))continue;let n=e.querySelector(`svg`);if(!n||!n.parentNode)continue;n.style.display=`none`,n.setAttribute(B,`1`);let r=document.createElement(`span`);r.setAttribute(z,`1`),r.setAttribute(`aria-hidden`,`true`),r.style.display=`inline-flex`,r.style.flex=`none`,r.innerHTML=`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect x="1.5" y="2.75" width="13" height="10.5" rx="1.75" stroke="currentColor" stroke-width="1.1"/><path d="M1.5 5.75h13" stroke="currentColor" stroke-width="1.1"/><circle cx="3.4" cy="4.25" r="0.65" fill="currentColor"/><circle cx="5.2" cy="4.25" r="0.65" fill="currentColor"/></svg>`,n.parentNode.insertBefore(r,n.nextSibling)}}function H(){try{V()}catch{}}function U(){if(!(typeof document>`u`)){for(let e of Array.from(document.querySelectorAll(`[${z}]`)))e.remove();for(let e of Array.from(document.querySelectorAll(`svg[${B}]`)))e.style.display=``,e.removeAttribute(B)}}function W(e){if(typeof document>`u`||typeof MutationObserver>`u`)return;H();let t=null;try{t=new MutationObserver(H),t.observe(document.body,{childList:!0,subtree:!0,characterData:!0})}catch{t=null}e.effect(()=>()=>{if(t){let e=t;t=null;try{e.disconnect()}catch{}}try{U()}catch{}})}let G=[`slots`];function K(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`浏览器`},L)),W(e),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},P))}return n.apply=K,n.inject=G,t.exports}});
|
|
2
2
|
//# sourceMappingURL=client.js.map
|
package/lib/index.js
CHANGED
|
@@ -57,6 +57,10 @@ const BROWSER_CONFIG_SCHEMA = {
|
|
|
57
57
|
executablePath: { type: "string" },
|
|
58
58
|
headless: { type: "boolean" },
|
|
59
59
|
userDataDir: { type: "string" },
|
|
60
|
+
viewport: {
|
|
61
|
+
type: "any",
|
|
62
|
+
optional: true
|
|
63
|
+
},
|
|
60
64
|
profiles: { type: "any" },
|
|
61
65
|
screenshotDir: { type: "string" },
|
|
62
66
|
downloadDir: { type: "string" },
|
|
@@ -3649,11 +3653,35 @@ async function loadPlaywright() {
|
|
|
3649
3653
|
throw new Error("dsh-browser: playwright-core is not installed. Run `dsh plugin --profile web add playwright-core` or install it in the profile.");
|
|
3650
3654
|
}
|
|
3651
3655
|
}
|
|
3656
|
+
/**
|
|
3657
|
+
* 解析 viewport 配置为 Playwright context 选项:
|
|
3658
|
+
* - "window" → viewport: null(关闭视口仿真,视口 = 浏览器窗口客户区,
|
|
3659
|
+
* 拖动窗口页面实时重排;此时 page.viewportSize() 返回 null,坐标换算
|
|
3660
|
+
* 一律走 window.innerWidth/innerHeight 兜底);
|
|
3661
|
+
* - { width, height } → 固定视口仿真(自定义尺寸);
|
|
3662
|
+
* - 缺省 → {}(Playwright 默认 1280×720 固定仿真,行为不变)。
|
|
3663
|
+
* 形状非法时抛中文配置错误(schema 层只声明 any,这里做精确校验)。
|
|
3664
|
+
*/
|
|
3665
|
+
function viewportOptionOf(config) {
|
|
3666
|
+
const v = config.viewport;
|
|
3667
|
+
if (v === void 0 || v === null) return {};
|
|
3668
|
+
if (v === "window") return { viewport: null };
|
|
3669
|
+
if (typeof v === "object") {
|
|
3670
|
+
const width = Number(v.width);
|
|
3671
|
+
const height = Number(v.height);
|
|
3672
|
+
if (Number.isInteger(width) && width > 0 && Number.isInteger(height) && height > 0) return { viewport: {
|
|
3673
|
+
width,
|
|
3674
|
+
height
|
|
3675
|
+
} };
|
|
3676
|
+
}
|
|
3677
|
+
throw new Error("dsh-browser: config.viewport 无效 — 只接受 \"window\"(视口跟随窗口)或 { width, height }(正整数)");
|
|
3678
|
+
}
|
|
3652
3679
|
async function launchProfile(name, config) {
|
|
3653
3680
|
const pw = await loadPlaywright();
|
|
3654
3681
|
const state = getProfileState(name);
|
|
3655
3682
|
const resolved = profileConfig(config, name);
|
|
3656
3683
|
const base = { headless: resolved.headless ?? false };
|
|
3684
|
+
const viewportOption = viewportOptionOf(resolved);
|
|
3657
3685
|
const candidates = [];
|
|
3658
3686
|
if (resolved.executablePath) candidates.push({
|
|
3659
3687
|
...base,
|
|
@@ -3684,7 +3712,8 @@ async function launchProfile(name, config) {
|
|
|
3684
3712
|
if (resolved.userDataDir) {
|
|
3685
3713
|
const context = await pw.chromium.launchPersistentContext(resolved.userDataDir, {
|
|
3686
3714
|
...options,
|
|
3687
|
-
headless: resolved.headless ?? false
|
|
3715
|
+
headless: resolved.headless ?? false,
|
|
3716
|
+
...viewportOption
|
|
3688
3717
|
});
|
|
3689
3718
|
await Promise.race([Promise.all(context.pages().map((p) => p.close().catch(() => {}))), new Promise((r) => setTimeout(r, 3e3))]);
|
|
3690
3719
|
state.browser = null;
|
|
@@ -3758,7 +3787,7 @@ async function newPage(config) {
|
|
|
3758
3787
|
return page;
|
|
3759
3788
|
}
|
|
3760
3789
|
if (browser !== null) {
|
|
3761
|
-
const page = await browser.newPage();
|
|
3790
|
+
const page = await browser.newPage(viewportOptionOf(resolved));
|
|
3762
3791
|
attachPage(page, resolved);
|
|
3763
3792
|
return page;
|
|
3764
3793
|
}
|
|
@@ -3933,7 +3962,7 @@ async function contextTool(config, args) {
|
|
|
3933
3962
|
if (contexts.has(name)) throw new Error(`context "${name}" 已存在;如需重建先 browser_context { action: "close", name }`);
|
|
3934
3963
|
if (state.browser === null && state.context === null) await launchProfile(currentProfile, config);
|
|
3935
3964
|
if (state.browser === null) throw new Error("持久模式(userDataDir)的浏览器只有单一 context,不支持多 context;需要多账号隔离请配置 profiles 多套 userDataDir");
|
|
3936
|
-
const context = await state.browser.newContext();
|
|
3965
|
+
const context = await state.browser.newContext(viewportOptionOf(profileConfig(config, currentProfile)));
|
|
3937
3966
|
const page = await context.newPage();
|
|
3938
3967
|
attachPage(page, profileConfig(config, currentProfile));
|
|
3939
3968
|
const id = registerTabPage(state, page);
|
|
@@ -4725,10 +4754,13 @@ async function screenshotTool(config, args, exec) {
|
|
|
4725
4754
|
maxAgeDays: config.screenshotMaxAgeDays,
|
|
4726
4755
|
maxCount: config.screenshotMaxCount
|
|
4727
4756
|
});
|
|
4728
|
-
const viewport = page.viewportSize() ?? {
|
|
4757
|
+
const viewport = page.viewportSize() ?? await page.evaluate(() => ({
|
|
4758
|
+
width: window.innerWidth,
|
|
4759
|
+
height: window.innerHeight
|
|
4760
|
+
})).catch(() => ({
|
|
4729
4761
|
width: 0,
|
|
4730
4762
|
height: 0
|
|
4731
|
-
};
|
|
4763
|
+
}));
|
|
4732
4764
|
return {
|
|
4733
4765
|
path: filePath,
|
|
4734
4766
|
bytes: buffer.length,
|
|
@@ -6538,9 +6570,16 @@ async function pickToolOnPage(page, config, args) {
|
|
|
6538
6570
|
const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 3e5) || 3e5, 1e3), 6e5);
|
|
6539
6571
|
const id = `pick-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6540
6572
|
const meta = screencastManager.getLastFrame()?.metadata ?? void 0;
|
|
6541
|
-
const viewport = (() => {
|
|
6573
|
+
const viewport = await (async () => {
|
|
6542
6574
|
try {
|
|
6543
|
-
|
|
6575
|
+
const size = page.viewportSize();
|
|
6576
|
+
if (size !== null) return size;
|
|
6577
|
+
const inner = await page.evaluate(() => ({
|
|
6578
|
+
width: window.innerWidth,
|
|
6579
|
+
height: window.innerHeight
|
|
6580
|
+
}));
|
|
6581
|
+
if (inner.width > 0 && inner.height > 0) return inner;
|
|
6582
|
+
return;
|
|
6544
6583
|
} catch {
|
|
6545
6584
|
return;
|
|
6546
6585
|
}
|
package/lib/mcp.js
CHANGED
|
@@ -63,6 +63,10 @@ const BROWSER_CONFIG_SCHEMA = {
|
|
|
63
63
|
executablePath: { type: "string" },
|
|
64
64
|
headless: { type: "boolean" },
|
|
65
65
|
userDataDir: { type: "string" },
|
|
66
|
+
viewport: {
|
|
67
|
+
type: "any",
|
|
68
|
+
optional: true
|
|
69
|
+
},
|
|
66
70
|
profiles: { type: "any" },
|
|
67
71
|
screenshotDir: { type: "string" },
|
|
68
72
|
downloadDir: { type: "string" },
|
|
@@ -3561,11 +3565,35 @@ async function loadPlaywright() {
|
|
|
3561
3565
|
throw new Error("dsh-browser: playwright-core is not installed. Run `dsh plugin --profile web add playwright-core` or install it in the profile.");
|
|
3562
3566
|
}
|
|
3563
3567
|
}
|
|
3568
|
+
/**
|
|
3569
|
+
* 解析 viewport 配置为 Playwright context 选项:
|
|
3570
|
+
* - "window" → viewport: null(关闭视口仿真,视口 = 浏览器窗口客户区,
|
|
3571
|
+
* 拖动窗口页面实时重排;此时 page.viewportSize() 返回 null,坐标换算
|
|
3572
|
+
* 一律走 window.innerWidth/innerHeight 兜底);
|
|
3573
|
+
* - { width, height } → 固定视口仿真(自定义尺寸);
|
|
3574
|
+
* - 缺省 → {}(Playwright 默认 1280×720 固定仿真,行为不变)。
|
|
3575
|
+
* 形状非法时抛中文配置错误(schema 层只声明 any,这里做精确校验)。
|
|
3576
|
+
*/
|
|
3577
|
+
function viewportOptionOf(config) {
|
|
3578
|
+
const v = config.viewport;
|
|
3579
|
+
if (v === void 0 || v === null) return {};
|
|
3580
|
+
if (v === "window") return { viewport: null };
|
|
3581
|
+
if (typeof v === "object") {
|
|
3582
|
+
const width = Number(v.width);
|
|
3583
|
+
const height = Number(v.height);
|
|
3584
|
+
if (Number.isInteger(width) && width > 0 && Number.isInteger(height) && height > 0) return { viewport: {
|
|
3585
|
+
width,
|
|
3586
|
+
height
|
|
3587
|
+
} };
|
|
3588
|
+
}
|
|
3589
|
+
throw new Error("dsh-browser: config.viewport 无效 — 只接受 \"window\"(视口跟随窗口)或 { width, height }(正整数)");
|
|
3590
|
+
}
|
|
3564
3591
|
async function launchProfile(name, config) {
|
|
3565
3592
|
const pw = await loadPlaywright();
|
|
3566
3593
|
const state = getProfileState(name);
|
|
3567
3594
|
const resolved = profileConfig(config, name);
|
|
3568
3595
|
const base = { headless: resolved.headless ?? false };
|
|
3596
|
+
const viewportOption = viewportOptionOf(resolved);
|
|
3569
3597
|
const candidates = [];
|
|
3570
3598
|
if (resolved.executablePath) candidates.push({
|
|
3571
3599
|
...base,
|
|
@@ -3596,7 +3624,8 @@ async function launchProfile(name, config) {
|
|
|
3596
3624
|
if (resolved.userDataDir) {
|
|
3597
3625
|
const context = await pw.chromium.launchPersistentContext(resolved.userDataDir, {
|
|
3598
3626
|
...options,
|
|
3599
|
-
headless: resolved.headless ?? false
|
|
3627
|
+
headless: resolved.headless ?? false,
|
|
3628
|
+
...viewportOption
|
|
3600
3629
|
});
|
|
3601
3630
|
await Promise.race([Promise.all(context.pages().map((p) => p.close().catch(() => {}))), new Promise((r) => setTimeout(r, 3e3))]);
|
|
3602
3631
|
state.browser = null;
|
|
@@ -3670,7 +3699,7 @@ async function newPage(config) {
|
|
|
3670
3699
|
return page;
|
|
3671
3700
|
}
|
|
3672
3701
|
if (browser !== null) {
|
|
3673
|
-
const page = await browser.newPage();
|
|
3702
|
+
const page = await browser.newPage(viewportOptionOf(resolved));
|
|
3674
3703
|
attachPage(page, resolved);
|
|
3675
3704
|
return page;
|
|
3676
3705
|
}
|
|
@@ -3845,7 +3874,7 @@ async function contextTool(config, args) {
|
|
|
3845
3874
|
if (contexts.has(name)) throw new Error(`context "${name}" 已存在;如需重建先 browser_context { action: "close", name }`);
|
|
3846
3875
|
if (state.browser === null && state.context === null) await launchProfile(currentProfile, config);
|
|
3847
3876
|
if (state.browser === null) throw new Error("持久模式(userDataDir)的浏览器只有单一 context,不支持多 context;需要多账号隔离请配置 profiles 多套 userDataDir");
|
|
3848
|
-
const context = await state.browser.newContext();
|
|
3877
|
+
const context = await state.browser.newContext(viewportOptionOf(profileConfig(config, currentProfile)));
|
|
3849
3878
|
const page = await context.newPage();
|
|
3850
3879
|
attachPage(page, profileConfig(config, currentProfile));
|
|
3851
3880
|
const id = registerTabPage(state, page);
|
|
@@ -4628,10 +4657,13 @@ async function screenshotTool(config, args, exec) {
|
|
|
4628
4657
|
maxAgeDays: config.screenshotMaxAgeDays,
|
|
4629
4658
|
maxCount: config.screenshotMaxCount
|
|
4630
4659
|
});
|
|
4631
|
-
const viewport = page.viewportSize() ?? {
|
|
4660
|
+
const viewport = page.viewportSize() ?? await page.evaluate(() => ({
|
|
4661
|
+
width: window.innerWidth,
|
|
4662
|
+
height: window.innerHeight
|
|
4663
|
+
})).catch(() => ({
|
|
4632
4664
|
width: 0,
|
|
4633
4665
|
height: 0
|
|
4634
|
-
};
|
|
4666
|
+
}));
|
|
4635
4667
|
return {
|
|
4636
4668
|
path: filePath,
|
|
4637
4669
|
bytes: buffer.length,
|
|
@@ -6437,9 +6469,16 @@ async function pickToolOnPage(page, config, args) {
|
|
|
6437
6469
|
const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 3e5) || 3e5, 1e3), 6e5);
|
|
6438
6470
|
const id = `pick-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6439
6471
|
const meta = screencastManager.getLastFrame()?.metadata ?? void 0;
|
|
6440
|
-
const viewport = (() => {
|
|
6472
|
+
const viewport = await (async () => {
|
|
6441
6473
|
try {
|
|
6442
|
-
|
|
6474
|
+
const size = page.viewportSize();
|
|
6475
|
+
if (size !== null) return size;
|
|
6476
|
+
const inner = await page.evaluate(() => ({
|
|
6477
|
+
width: window.innerWidth,
|
|
6478
|
+
height: window.innerHeight
|
|
6479
|
+
}));
|
|
6480
|
+
if (inner.width > 0 && inner.height > 0) return inner;
|
|
6481
|
+
return;
|
|
6443
6482
|
} catch {
|
|
6444
6483
|
return;
|
|
6445
6484
|
}
|
|
@@ -40,6 +40,21 @@ export declare function downloadsOf(name: string): DownloadEntry[];
|
|
|
40
40
|
export declare function historyOf(name: string): NavEntry[];
|
|
41
41
|
export declare function attachPage(page: Page, config: BrowserConfig): void;
|
|
42
42
|
export declare function loadPlaywright(): Promise<typeof import("playwright-core")>;
|
|
43
|
+
/**
|
|
44
|
+
* 解析 viewport 配置为 Playwright context 选项:
|
|
45
|
+
* - "window" → viewport: null(关闭视口仿真,视口 = 浏览器窗口客户区,
|
|
46
|
+
* 拖动窗口页面实时重排;此时 page.viewportSize() 返回 null,坐标换算
|
|
47
|
+
* 一律走 window.innerWidth/innerHeight 兜底);
|
|
48
|
+
* - { width, height } → 固定视口仿真(自定义尺寸);
|
|
49
|
+
* - 缺省 → {}(Playwright 默认 1280×720 固定仿真,行为不变)。
|
|
50
|
+
* 形状非法时抛中文配置错误(schema 层只声明 any,这里做精确校验)。
|
|
51
|
+
*/
|
|
52
|
+
export declare function viewportOptionOf(config: BrowserConfig): {
|
|
53
|
+
viewport: {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
} | null;
|
|
57
|
+
} | Record<string, never>;
|
|
43
58
|
export declare function launchProfile(name: string, config: BrowserConfig): Promise<void>;
|
|
44
59
|
/** 当前 profile 状态(未启动时创建空状态)。 */
|
|
45
60
|
export declare function stateOf(): ProfileState;
|
|
@@ -5,6 +5,16 @@ export interface BrowserConfig {
|
|
|
5
5
|
executablePath?: string;
|
|
6
6
|
headless?: boolean;
|
|
7
7
|
userDataDir?: string;
|
|
8
|
+
/**
|
|
9
|
+
* 页面视口:"window"(视口跟随浏览器窗口大小,关闭视口仿真;有头模式
|
|
10
|
+
* 拖窗口页面实时重排,坐标换算以 window.innerWidth/innerHeight 为基准)|
|
|
11
|
+
* { width, height }(固定视口,如 1920×1080)。缺省 = Playwright 默认
|
|
12
|
+
* 1280×720 固定仿真(不随窗口变化,测试确定性最佳)。
|
|
13
|
+
*/
|
|
14
|
+
viewport?: "window" | {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
8
18
|
profiles?: Record<string, Partial<BrowserConfig>>;
|
|
9
19
|
screenshotDir?: string;
|
|
10
20
|
downloadDir?: string;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-browser-view client half:
|
|
3
|
-
* 1. "settings.section" -> A "
|
|
3
|
+
* 1. "settings.section" -> A "浏览器" page in official dsh settings dialog
|
|
4
|
+
* (nav icon patched to a browser glyph by ./navIcon.ts — the shell hardcodes
|
|
5
|
+
* a gear for unknown section ids).
|
|
4
6
|
* 2. "shell.overlay" -> Universal floating widget (FAB + live screenshot + modal).
|
|
5
7
|
*/
|
|
6
8
|
import type { ClientContext } from "@deepseek-ai/dsh-client-runtime/client";
|
|
@@ -16,7 +18,7 @@ declare module "@deepseek-ai/dsh-client-ui-slots" {
|
|
|
16
18
|
export declare const inject: string[];
|
|
17
19
|
/**
|
|
18
20
|
* Register the browser extension components:
|
|
19
|
-
* 1. settings.section -> "
|
|
21
|
+
* 1. settings.section -> "浏览器" page in settings
|
|
20
22
|
* 2. shell.overlay -> Universal floating live screenshot widget (Desktop & Web)
|
|
21
23
|
*/
|
|
22
24
|
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClientContext } from "@deepseek-ai/dsh-client-runtime/client";
|
|
2
|
+
/**
|
|
3
|
+
* 安装设置导航图标补丁:立即补一次,再以 MutationObserver 持续复补丁
|
|
4
|
+
* (外壳重渲染导航时会还原齿轮)。通过 ctx.effect 注册卸载清理。
|
|
5
|
+
*/
|
|
6
|
+
export declare function installSettingsNavIconPatch(ctx: ClientContext): void;
|
|
7
|
+
//# sourceMappingURL=navIcon.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-view",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "DSH 插件:共享 Playwright 浏览器自动化(38 个 browser_* 工具,含 what 意图语义定位(一次调用替代快照→查找→点击)、SoM 标徽看图点击与零命中截图兜底、SPA ref 软失效重锚定、open Shadow DOM 组合树穿透、跨 Frame/IFrame 定位、视口感知动态快照、批量表单智能填充、点击效果验证与定位失败遥测、Playwright 规范代码导出、批处理、视觉回归、选择器自愈、增量快照)+ 人机协作(面板点选教学 browser_pick_element、人工接管等待、敏感操作审批硬拦截、人机混合录制、面板一键重跑)+ CDP 流式推流、拟人化轨迹与高斯击键、全景时光机、企业富控件矩阵。另附独立 MCP server 入口(bin dsh-browser-mcp,stdio),任意 MCP 客户端可直接调用全部工具。运行时依赖 playwright-core 与 @modelcontextprotocol/sdk(仅 MCP 入口,不进 DSH 插件加载链)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"lint": "biome check src test",
|
|
40
40
|
"format": "biome format --write src test",
|
|
41
41
|
"test": "vitest run",
|
|
42
|
-
"test:algo": "tsx scripts/test-dev-server-algo-config.ts",
|
|
43
42
|
"test:enterprise": "tsx scripts/test-dev-server-enterprise.ts",
|
|
44
43
|
"test:energy": "tsx scripts/test-dev-server-energy.ts",
|
|
45
44
|
"test:shadow": "tsx scripts/test-dev-server-shadow.ts",
|