dsh-browser-view 0.8.3 → 0.10.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 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
@@ -58,6 +61,8 @@ dsh plugin --profile web add dsh-browser-view
58
61
 
59
62
  **工具集裁剪(toolset)**:默认 `all` 注册全部工具。日常浏览/操作/人机协作为主的会话可设 `toolset: core`——只注册 25 个高频核心工具(打开/快照/查找/点击/输入/等待/断言/网络/标签/下载上传/登录等待/人工接管/审批/点选教学/诊断),`browser_record / replay / report / extract / form / profile / context / cookies` 等低频工具不注册、不占 systemPrompt 常驻 token(低频工具的录制管理界面不受影响,已有录制仍可回放,只是 agent 会话内无这些工具)。需要做回归测试或数据抓取时把配置改回 `all` 并重启 DSH 即可。
60
63
 
64
+ **GUI 配置(免改 YAML)**:DSH 侧栏「插件」→ 打开 `dsh-browser-view` 组合包详情页,描述下方即配置面板(通道 / 无头 / userDataDir / 视口 / 超时 / toolset / 审批模式 / 截图保留)。保存经平台 configEditor 写回当前 profile 的 `cordis.patch.yml` 并由 Loader 重组——启用 HMR 的 profile 立即重载生效(运行中的浏览器会话随插件重启),未启用 HMR 时重启生效;留空/选「默认」即删除该键、回落组合包默认值。环境无 configEditor 服务(如独立 MCP 模式)时面板自动降级只读。
65
+
61
66
  ## 浏览器工具
62
67
 
63
68
  | 工具 | 说明 |
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}}async function b(e){try{let t=await fetch(`${e}/config`,{cache:`no-store`});if(!t.ok)return null;let n=await t.json();return typeof n?.editable==`boolean`?n:null}catch{return null}}async function x(e,t){try{let n=await fetch(`${e}/config`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({changes:t})});if(!n.ok){let e=await n.json().catch(()=>void 0);return{ok:!1,...e?.error===void 0?{error:`HTTP ${n.status}`}:{error:e.error}}}return{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function S(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 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 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 w(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 T(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 E={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},D={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function O(){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 k(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function A({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 j({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 M({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)(A,{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)(j,{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`}},k(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 N({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)(j,{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`}},k(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function P({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 F({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 I(){let e=O(),t=e?D:E,[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:b,rerunMsg:x,rerunLast:k}=S(n),{pendingConfirm:A,resolveConfirm:j}=C(n),{loginWait:I,confirmLoginDone:L}=w(n),{pendingPick:R}=T(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)(F,{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)(M,{status:g,ops:_,lastRun:y,agentBusy:b,rerunMsg:x,pendingConfirm:A,pendingPick:R,shotSrc:z,streamSrc:B,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{A&&j(A.id,e)},onRerunLast:k}),l&&(0,r.createElement)(N,{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)(P,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function L(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 R(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 z(e){let t=O(),n=t?D:E,i=R(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}=L(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 B={channel:``,headless:``,userDataDir:``,viewportMode:``,viewportWidth:``,viewportHeight:``,timeoutMs:``,toolset:``,confirmMode:``,screenshotMaxAgeDays:``,screenshotMaxCount:``};function V(e){let t={...e.inherited,...e.override},n=t.viewport,r=``,i=``,a=``;return n===`window`?r=`window`:typeof n==`object`&&n&&(r=`custom`,i=String(n.width??``),a=String(n.height??``)),{channel:typeof t.channel==`string`?t.channel===``?`bundled`:t.channel:``,headless:typeof t.headless==`boolean`?String(t.headless):``,userDataDir:typeof t.userDataDir==`string`?t.userDataDir:``,viewportMode:r,viewportWidth:i,viewportHeight:a,timeoutMs:typeof t.timeoutMs==`number`?String(t.timeoutMs):``,toolset:typeof t.toolset==`string`?t.toolset:``,confirmMode:typeof t.confirmMode==`string`?t.confirmMode:``,screenshotMaxAgeDays:typeof t.screenshotMaxAgeDays==`number`?String(t.screenshotMaxAgeDays):``,screenshotMaxCount:typeof t.screenshotMaxCount==`number`?String(t.screenshotMaxCount):``}}function H(e){let t=e=>e.trim()===``?null:Number(e),n={channel:e.channel===``?null:e.channel===`bundled`?``:e.channel,headless:e.headless===``?null:e.headless===`true`,userDataDir:e.userDataDir.trim()===``?null:e.userDataDir.trim(),timeoutMs:t(e.timeoutMs),toolset:e.toolset===``?null:e.toolset,confirmMode:e.confirmMode===``?null:e.confirmMode,screenshotMaxAgeDays:t(e.screenshotMaxAgeDays),screenshotMaxCount:t(e.screenshotMaxCount)};return n.viewport=e.viewportMode===``?null:e.viewportMode===`window`?`window`:{width:Number(e.viewportWidth),height:Number(e.viewportHeight)},n}function U(e){let t=O(),n=t?D:E,i=R(n),[o,s]=(0,r.useState)(`/browser`),[c,l]=(0,r.useState)(!0),[u,d]=(0,r.useState)(!1),[f,p]=(0,r.useState)(``),[m,h]=(0,r.useState)(B),[g,_]=(0,r.useState)(!1),[v,y]=(0,r.useState)(null);if((0,r.useEffect)(()=>{let e=!0;return(async()=>{let t=(await a()).replace(/\/+$/,``);if(!e)return;s(t);let n=await b(t);e&&(n===null?p(`无法读取插件配置(宿主 API 不可达)`):(d(n.editable),n.editable||p(n.reason??`当前环境不支持在线编辑`),h(V(n))),l(!1))})(),()=>{e=!1}},[]),e.view===`summary`)return null;let S={height:`28px`,padding:`0 8px`,border:`1px solid ${n.border}`,borderRadius:`6px`,background:`transparent`,color:n.primary,font:`inherit`,fontSize:`13px`,minWidth:0},C=(e,t,i)=>(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`}},(0,r.createElement)(`span`,{style:{fontSize:`13px`,color:n.primary,fontWeight:500}},e),t,i===void 0?null:(0,r.createElement)(`span`,{style:{fontSize:`12px`,color:n.tertiary}},i)),w=e=>t=>{let n=t?.target?.value??``;h(t=>({...t,[e]:n})),y(null)},T=(e,t)=>(0,r.createElement)(`select`,{value:m[e],onChange:w(e),style:S,disabled:!u},...t.map(([e,t])=>(0,r.createElement)(`option`,{key:e,value:e},t))),k=(e,t)=>(0,r.createElement)(`input`,{value:m[e],onChange:w(e),placeholder:t,style:S,disabled:!u}),A=async()=>{_(!0),y(null);let e=await x(o,H(m));_(!1),e.ok?y({ok:!0,text:`已保存并经 Loader 应用;浏览器会话随插件重载重启,未生效时请重启 DSH`}):y({ok:!1,text:`保存失败: ${e.error??`未知错误`}`})};return(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`12px`,padding:`4px 0`}},c?(0,r.createElement)(`span`,{style:{fontSize:`13px`,color:n.tertiary}},`读取配置中…`):u?null:(0,r.createElement)(`span`,{style:{fontSize:`13px`,color:n.tertiary}},`只读: ${f}(可手改 profile 的 cordis.patch.yml)`),C(`浏览器通道`,T(`channel`,[[``,`默认(auto:依次尝试 chrome → msedge → chromium)`],[`auto`,`auto`],[`chrome`,`Chrome`],[`msedge`,`Edge`],[`chromium`,`Chromium`],[`bundled`,`Playwright 内置 Chromium`]])),C(`无头模式`,T(`headless`,[[``,`默认(有头窗口,本机调试/人工登录)`],[`false`,`有头`],[`true`,`无头(服务器场景)`]])),C(`登录态持久化目录 (userDataDir)`,k(`userDataDir`,`留空 = 不持久化`),`设置后 Cookie/localStorage 跨重启保留`),C(`页面视口`,T(`viewportMode`,[[``,`默认(1280×720 固定仿真,测试确定性最佳)`],[`window`,`跟随浏览器窗口大小`],[`custom`,`自定义固定尺寸`]])),m.viewportMode===`custom`?(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,alignItems:`center`}},(0,r.createElement)(`input`,{value:m.viewportWidth,onChange:w(`viewportWidth`),placeholder:`宽度,如 1920`,style:{...S,width:`120px`}}),(0,r.createElement)(`span`,{style:{color:n.tertiary}},`×`),(0,r.createElement)(`input`,{value:m.viewportHeight,onChange:w(`viewportHeight`),placeholder:`高度,如 1080`,style:{...S,width:`120px`}})):null,C(`操作超时 (timeoutMs,毫秒)`,k(`timeoutMs`,`默认 30000`)),C(`工具集 (toolset)`,T(`toolset`,[[``,`默认(all:全量工具)`],[`all`,`all(全量)`],[`core`,`core(高频核心集,降 systemPrompt 常驻 token)`]])),C(`敏感操作审批 (confirmMode)`,T(`confirmMode`,[[``,`默认(off)`],[`off`,`off(不主动拦截)`],[`manual`,`manual(提示 agent 先调用 browser_confirm)`],[`auto`,`auto(宿主层硬拦截提交/删除/支付类点击)`]])),C(`截图保留天数 (screenshotMaxAgeDays)`,k(`screenshotMaxAgeDays`,`默认 7;0 = 不按时间清理`)),C(`截图保留数量 (screenshotMaxCount)`,k(`screenshotMaxCount`,`默认 200;0 = 不按数量清理`)),(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`10px`}},(0,r.createElement)(`button`,{onClick:()=>void A(),disabled:!u||g,style:{...i.btn,height:`30px`,padding:`0 16px`,...u&&!g?{background:t?`rgba(99, 102, 241, 0.25)`:`rgba(99, 102, 241, 0.12)`,color:n.primary}:{opacity:.5,cursor:`not-allowed`}}},g?`保存中…`:`保存配置`),v===null?null:(0,r.createElement)(`span`,{style:{fontSize:`12px`,color:v.ok?n.secondary:n.danger}},v.text)))}let W=[`浏览器`,`🌐 浏览器`],G=`data-dsh-browser-nav-icon`,K=`data-dsh-browser-nav-hidden`;function q(){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(!W.some(e=>t===e)||e.querySelector(`[${G}]`))continue;let n=e.querySelector(`svg`);if(!n||!n.parentNode)continue;n.style.display=`none`,n.setAttribute(K,`1`);let r=document.createElement(`span`);r.setAttribute(G,`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 J(){try{q()}catch{}}function Y(){if(!(typeof document>`u`)){for(let e of Array.from(document.querySelectorAll(`[${G}]`)))e.remove();for(let e of Array.from(document.querySelectorAll(`svg[${K}]`)))e.style.display=``,e.removeAttribute(K)}}function X(e){if(typeof document>`u`||typeof MutationObserver>`u`)return;J();let t=null;try{t=new MutationObserver(J),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{Y()}catch{}})}let Z=[`slots`];function Q(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`浏览器`},z)),X(e),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},I)),e.slots.inject(`plugins.bundle.config`,()=>e.slots.register({name:`plugins.bundle.config`,key:`dsh-browser-view`},U))}return n.apply=Q,n.inject=Z,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
- return page.viewportSize() ?? void 0;
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
  }
@@ -10404,6 +10443,95 @@ function registerTools(ctx, config) {
10404
10443
  for (const toolList of toolFactories) for (const def of toolList) register(def);
10405
10444
  }
10406
10445
  //#endregion
10446
+ //#region lib/types/browser/config-api.js
10447
+ function configEditorOf(ctx) {
10448
+ const editor = ctx.get("configEditor") ?? ctx.configEditor;
10449
+ if (editor !== void 0 && typeof editor.entries === "function" && typeof editor.configuration === "function" && typeof editor.edit === "function") return editor;
10450
+ }
10451
+ /** 定位本插件的 profile 条目(按包名;恰一个才可编辑,与平台「唯一定位」约束一致)。 */
10452
+ function findOwnEntry(editor) {
10453
+ let entries;
10454
+ try {
10455
+ entries = editor.entries();
10456
+ } catch {
10457
+ return {
10458
+ ok: false,
10459
+ reason: "读取 profile 条目失败"
10460
+ };
10461
+ }
10462
+ const own = entries.filter((e) => e?.options?.name === "dsh-browser-view");
10463
+ if (own.length === 0) return {
10464
+ ok: false,
10465
+ reason: "当前 profile 中未找到本插件条目"
10466
+ };
10467
+ if (own.length > 1) return {
10468
+ ok: false,
10469
+ reason: "profile 中存在多个 dsh-browser-view 条目,无法唯一定位,请手改 cordis.patch.yml"
10470
+ };
10471
+ return {
10472
+ ok: true,
10473
+ entry: own[0]
10474
+ };
10475
+ }
10476
+ function getPluginConfig(ctx, effective) {
10477
+ const editor = configEditorOf(ctx);
10478
+ if (editor === void 0) return {
10479
+ editable: false,
10480
+ reason: "当前环境无 configEditor 服务(非 DSH profile 宿主)",
10481
+ override: {},
10482
+ inherited: {},
10483
+ effective
10484
+ };
10485
+ const found = findOwnEntry(editor);
10486
+ if (!found.ok) return {
10487
+ editable: false,
10488
+ reason: found.reason,
10489
+ override: {},
10490
+ inherited: {},
10491
+ effective
10492
+ };
10493
+ let override = {};
10494
+ let inherited = {};
10495
+ try {
10496
+ const row = editor.configuration().find((r) => r.entry === found.entry);
10497
+ override = { ...row?.override ?? {} };
10498
+ inherited = { ...row?.inherited ?? {} };
10499
+ } catch {}
10500
+ return {
10501
+ editable: true,
10502
+ override,
10503
+ inherited,
10504
+ effective
10505
+ };
10506
+ }
10507
+ /**
10508
+ * 校验并持久化配置变更。抛错(中文消息)即拒绝,不写盘。
10509
+ * - 只接受 schema 声明过的键;
10510
+ * - 值按 BROWSER_CONFIG_SCHEMA 校验(viewport 额外走 viewportOptionOf 形状校验);
10511
+ * - 校验以「合并默认值后的完整候选」进行,与启动时 resolveConfig 口径一致。
10512
+ */
10513
+ async function savePluginConfig(ctx, changes) {
10514
+ const editor = configEditorOf(ctx);
10515
+ if (editor === void 0) throw new Error("当前环境无 configEditor 服务,无法保存配置");
10516
+ const found = findOwnEntry(editor);
10517
+ if (!found.ok) throw new Error(found.reason);
10518
+ for (const key of Object.keys(changes)) if (!(key in BROWSER_CONFIG_SCHEMA)) throw new Error(`未知配置键: ${key}`);
10519
+ await editor.edit(found.entry, (current) => {
10520
+ const next = { ...current ?? {} };
10521
+ for (const [key, value] of Object.entries(changes)) if (value === null) delete next[key];
10522
+ else next[key] = value;
10523
+ const candidate = {
10524
+ ...BROWSER_DEFAULT_CONFIG,
10525
+ ...next
10526
+ };
10527
+ const errors = validateConfig(BROWSER_CONFIG_SCHEMA, candidate);
10528
+ if (errors.length > 0) throw new Error(`配置无效 — ${errors.join("; ")}`);
10529
+ if (next.viewport !== void 0) viewportOptionOf(next);
10530
+ return next;
10531
+ });
10532
+ return { saved: true };
10533
+ }
10534
+ //#endregion
10407
10535
  //#region lib/types/browser/ui.js
10408
10536
  /** 展示层参数截断:单字段 JSON 超过 300 字符截断(仅用于面板轨迹显示,不进录制)。 */
10409
10537
  function truncateForDisplay(args) {
@@ -10675,6 +10803,23 @@ function registerBrowserApi(ctx, config, base) {
10675
10803
  }
10676
10804
  return;
10677
10805
  }
10806
+ if (path === `${base}/config`) {
10807
+ if ((req.method ?? "GET") === "POST") {
10808
+ const body = await readJsonBody(req).catch(() => void 0);
10809
+ if (typeof body?.changes !== "object" || body.changes === null || Array.isArray(body.changes)) {
10810
+ sendJson(res, 400, { error: "body.changes 必须是对象(key → 值;null = 删除该键回默认)" });
10811
+ return;
10812
+ }
10813
+ try {
10814
+ sendJson(res, 200, await savePluginConfig(ctx, body.changes));
10815
+ } catch (e) {
10816
+ sendJson(res, 400, { error: e instanceof Error ? e.message : String(e) });
10817
+ }
10818
+ return;
10819
+ }
10820
+ sendJson(res, 200, getPluginConfig(ctx, config));
10821
+ return;
10822
+ }
10678
10823
  sendText(res, 404, "not found");
10679
10824
  }
10680
10825
  }));
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
- return page.viewportSize() ?? void 0;
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
  }
@@ -0,0 +1,25 @@
1
+ import type { ReefContext } from "../lib/types.js";
2
+ import type { BrowserConfig } from "./types.js";
3
+ export interface PluginConfigView {
4
+ editable: boolean;
5
+ reason?: string;
6
+ /** profile 层显式覆盖值(用户可编辑的部分)。 */
7
+ override: Record<string, unknown>;
8
+ /** 下层(组合包 patch)组合出的值。 */
9
+ inherited: Record<string, unknown>;
10
+ /** 运行中生效的完整配置(resolveConfig 之后)。 */
11
+ effective: BrowserConfig;
12
+ }
13
+ export declare function getPluginConfig(ctx: ReefContext, effective: BrowserConfig): PluginConfigView;
14
+ /** 配置变更集:key → 新值;值为 null 表示从条目配置中删除(回落到继承/默认)。 */
15
+ export type PluginConfigChanges = Record<string, unknown>;
16
+ /**
17
+ * 校验并持久化配置变更。抛错(中文消息)即拒绝,不写盘。
18
+ * - 只接受 schema 声明过的键;
19
+ * - 值按 BROWSER_CONFIG_SCHEMA 校验(viewport 额外走 viewportOptionOf 形状校验);
20
+ * - 校验以「合并默认值后的完整候选」进行,与启动时 resolveConfig 口径一致。
21
+ */
22
+ export declare function savePluginConfig(ctx: ReefContext, changes: PluginConfigChanges): Promise<{
23
+ saved: true;
24
+ }>;
25
+ //# sourceMappingURL=config-api.d.ts.map
@@ -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,4 +1,4 @@
1
- import type { BrowserOp, BrowserStatus, LatestRun, NavHistoryItem, PendingConfirm, PendingLoginWait, PendingPick, RecordingDetail, RecordingRow } from "./types.js";
1
+ import type { BrowserOp, BrowserStatus, LatestRun, NavHistoryItem, PendingConfirm, PendingLoginWait, PendingPick, PluginConfigState, RecordingDetail, RecordingRow } from "./types.js";
2
2
  /** 获取插件后端 API 基路径 (默认为 /browser) */
3
3
  export declare function getBasePath(): Promise<string>;
4
4
  export declare function fetchStatus(base: string): Promise<BrowserStatus | null>;
@@ -24,4 +24,11 @@ export declare function triggerRerun(base: string, source: string, retryOf?: str
24
24
  export declare function fetchRecordings(base: string): Promise<RecordingRow[]>;
25
25
  export declare function fetchRecordingDetail(base: string, name: string): Promise<RecordingDetail | null>;
26
26
  export declare function deleteRecordingApi(base: string, name: string): Promise<boolean>;
27
+ /** 读取插件配置(插件页配置面板)。 */
28
+ export declare function fetchPluginConfig(base: string): Promise<PluginConfigState | null>;
29
+ /** 保存插件配置变更(value 为 null 表示删除该键回默认)。 */
30
+ export declare function savePluginConfigApi(base: string, changes: Record<string, unknown>): Promise<{
31
+ ok: boolean;
32
+ error?: string;
33
+ }>;
27
34
  //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,13 @@
1
+ /** plugins.bundle.config 的 owner props(契约见 dsh-client-ui-plugin-manager slot-contract)。 */
2
+ export interface PluginConfigViewProps {
3
+ view: "summary" | "page";
4
+ }
5
+ export declare function BrowserPluginConfigPage(props: PluginConfigViewProps): import("react").DetailedReactHTMLElement<{
6
+ style: {
7
+ display: "flex";
8
+ flexDirection: "column";
9
+ gap: string;
10
+ padding: string;
11
+ };
12
+ }, HTMLElement> | null;
13
+ //# sourceMappingURL=PluginConfigForm.d.ts.map
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * dsh-browser-view client half:
3
- * 1. "settings.section" -> A "🌐 浏览器" page in official dsh settings dialog.
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";
@@ -10,13 +12,21 @@ declare module "@deepseek-ai/dsh-client-ui-slots" {
10
12
  kind: "list";
11
13
  scope: "root";
12
14
  };
15
+ /** 侧栏「插件」页的组合包配置区(key = 包名;契约属 dsh-client-ui-plugin-manager)。 */
16
+ "plugins.bundle.config": {
17
+ kind: "keyed";
18
+ scope: "root";
19
+ owner: {
20
+ view: "summary" | "page";
21
+ };
22
+ };
13
23
  }
14
24
  }
15
25
  /** Required services (cordis fiber inject). */
16
26
  export declare const inject: string[];
17
27
  /**
18
28
  * Register the browser extension components:
19
- * 1. settings.section -> "🌐 浏览器" page in settings
29
+ * 1. settings.section -> "浏览器" page in settings
20
30
  * 2. shell.overlay -> Universal floating live screenshot widget (Desktop & Web)
21
31
  */
22
32
  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
@@ -78,4 +78,12 @@ export interface LatestRun {
78
78
  };
79
79
  retryOf?: string;
80
80
  }
81
+ /** 插件配置视图(GET {base}/config):override = profile 层显式值,effective = 生效值。 */
82
+ export interface PluginConfigState {
83
+ editable: boolean;
84
+ reason?: string;
85
+ override: Record<string, unknown>;
86
+ inherited: Record<string, unknown>;
87
+ effective: Record<string, unknown>;
88
+ }
81
89
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-view",
3
- "version": "0.8.3",
3
+ "version": "0.10.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",