dsh-browser-view 0.3.1 → 0.3.4

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/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={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},a={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 s(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function c(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(){let e=o(),t=e?a:i,[n,c]=(0,r.useState)(`/browser`),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[p,m]=(0,r.useState)(null),[h,g]=(0,r.useState)([]),[_,v]=(0,r.useState)([]),[y,b]=(0,r.useState)(Date.now()),[x,S]=(0,r.useState)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(!1);(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&c(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0,t=()=>{fetch(`${n}/status`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&(m(t),t?.open&&b(Date.now()))}).catch(()=>{e&&m(null)}),fetch(`${n}/log`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&t&&Array.isArray(t.calls)&&g(t.calls.slice(0,8))}).catch(()=>{}),fetch(`${n}/runs/latest`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&S(t?.run??null)}).catch(()=>{})};t();let r=setInterval(t,3e3);return()=>{e=!1,clearInterval(r)}},[n]),(0,r.useEffect)(()=>{let e=!0,t=()=>{fetch(`${n}/confirm`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&w(t?.confirm??null)}).catch(()=>{})};t();let r=setInterval(t,1e3);return()=>{e=!1,clearInterval(r)}},[n]);let D=e=>{let t=C?.id;t&&(w(null),fetch(`${n}/confirm`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,decision:e})}).catch(()=>{}))};(0,r.useEffect)(()=>{if(!d)return;let e=!0,t=()=>{fetch(`${n}/history`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&t&&Array.isArray(t.history)&&v(t.history)}).catch(()=>{}),b(Date.now())};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[d,n]),(0,r.useEffect)(()=>{if(!d)return;let e=e=>{e.key===`Escape`&&f(!1)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[d]);let O=`${n}/screenshot?v=${y}`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>u(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.25)`,pointerEvents:`auto`,userSelect:`none`}},`🌐`),l&&(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`66px`,width:`320px`,maxHeight:`72vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${t.border}`,borderRadius:`12px`,background:e?`#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:p?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:t.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:t.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},p===null?`未启用`:p.open?`${p.tabs??1} 标签 · ${(p.url||``).slice(0,20)}`:`未打开`)),C?(0,r.createElement)(`div`,{style:{padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #ef4444`,background:e?`rgba(239, 68, 68, 0.12)`:`rgba(239, 68, 68, 0.07)`,color:e?`#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`}},C.reason,C.tool?` (${C.tool})`:``),(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,marginTop:`6px`}},(0,r.createElement)(`button`,{onClick:()=>D(`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:()=>D(`denied`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#dc2626`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`拒绝`))):null,p?.pause?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #f59e0b`,background:e?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:e?`#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`}},`等待人工操作: ${p.pause.instruction}`)):null,p?.open?(0,r.createElement)(`img`,{src:O,alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:()=>f(!0),style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${t.border}`,cursor:`pointer`,background:e?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:e?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),h.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${t.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${h.length} 条)`),h.map((e,n)=>(0,r.createElement)(`div`,{key:n,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:t.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:t.tertiary,fontSize:`11px`,flex:`none`}},s(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}`:``}`)))),x!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${t.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:t.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,cursor:`pointer`},title:`runId=${x.runId} · 点击查看报告`,onClick:()=>E(!0)},(0,r.createElement)(`span`,{style:{flex:`none`,color:t.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:x.failed===0?`#10b981`:`#ef4444`}},x.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${x.source}${x.row===void 0?``:` 行${x.row}`} ${x.passed}/${x.total}${x.healedCount>0?` · 自愈${x.healedCount}`:``} · ${new Date(x.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`))),d&&(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&f(!1)},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 ${t.border}`,borderRadius:`12px`,background:e?`#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:t.primary,fontSize:`14px`,fontWeight:600}},p?.open?`${p.title||``} (${p.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:()=>f(!1),style:{width:`28px`,height:`28px`,border:`1px solid ${t.border}`,borderRadius:`6px`,background:`transparent`,color:t.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),p?.open?(0,r.createElement)(`img`,{src:O,alt:`浏览器大图`,style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:`1px solid ${t.border}`,background:e?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),_.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${t.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${_.length} 条,点击在新标签打开)`),_.map((e,n)=>(0,r.createElement)(`a`,{key:n,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:t.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:t.tertiary,flex:`none`}},s(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url)))))),T&&(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&E(!1)},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 ${t.border}`,borderRadius:`12px`,background:e?`#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,color:t.primary,fontSize:`14px`,fontWeight:600}},`回归报告${x===null?``:` — ${x.source}`}(运行趋势 / 失败明细 / 自愈清单)`),(0,r.createElement)(`button`,{type:`button`,onClick:()=>E(!1),style:{width:`28px`,height:`28px`,border:`1px solid ${t.border}`,borderRadius:`6px`,background:`transparent`,color:t.secondary,cursor:`pointer`}},`✕`)),(0,r.createElement)(`iframe`,{src:`${n}/runs/report`,title:`回归报告`,style:{flex:1,width:`100%`,border:`1px solid ${t.border}`,borderRadius:`8px`,background:`#fff`}}))))}function u(e){let t=o(),n=t?a:i,s=c(n),[l,u]=(0,r.useState)(`/browser`),[d,f]=(0,r.useState)(null),[p,m]=(0,r.useState)(``),[h,g]=(0,r.useState)({});(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&u(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0;return f(null),m(``),fetch(`${l}/recordings`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&f(t.recordings??[])}).catch(t=>{e&&m(t instanceof Error?t.message:String(t))}),()=>{e=!1}},[l]);let _=e=>{g(t=>{let n=t[e];if(n!==void 0&&n!==`loading`){let n={...t};return delete n[e],n}return fetch(`${l}/recordings/detail?name=${encodeURIComponent(e)}`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>g(n=>({...n,[e]:t}))).catch(()=>g(t=>({...t,[e]:`failed`}))),{...t,[e]:`loading`}})},v=e=>{fetch(`${l}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:e})}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(()=>{f(t=>t===null?t:t.filter(t=>t.name!==e)),g(t=>{let n={...t};return delete n[e],n})}).catch(()=>{})};return(0,r.createElement)(`div`,{style:s.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:`38px`,height:`38px`,borderRadius:`10px`,display:`flex`,alignItems:`center`,justifyContent:`center`,background:t?`#222938`:`#eef2f6`,border:`1px solid ${n.border}`,fontSize:`20px`,flexShrink:0}},`🌐`),(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}},`共享 Playwright 浏览器自动化、测试原语、操作轨迹与回归录制管理`))),(0,r.createElement)(`div`,{style:s.heading},`已保存的操作录制 (agent 在对话中用 browser_record save <name> 创建,存储于 $DSH_HOME/.dsh-browser/recordings)`),p?(0,r.createElement)(`div`,{style:s.empty},`加载失败: ${p}`):d===null?(0,r.createElement)(`div`,{style:s.empty},`加载中…`):d.length===0?(0,r.createElement)(`div`,{style:s.empty},`暂无录制。让 agent 跑完一轮流程后 browser_record save <name> 创建。`):d.map(e=>{let t=h[e.name];return(0,r.createElement)(`div`,{key:e.name,style:s.itemBox},(0,r.createElement)(`div`,{style:s.row},(0,r.createElement)(`span`,{style:s.label,title:`点击展开步骤`,onClick:()=>_(e.name)},e.name),(0,r.createElement)(`span`,{style:s.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleString()}`),(0,r.createElement)(`button`,{style:s.btn,onClick:()=>_(e.name)},t===void 0?`展开`:`收起`),(0,r.createElement)(`button`,{style:s.delBtn,onClick:()=>v(e.name)},`删除`)),t===void 0?null:t===`loading`?(0,r.createElement)(`div`,{style:s.empty},`加载中…`):(0,r.createElement)(`div`,{style:s.stepsBox},t===`failed`?(0,r.createElement)(`div`,{style:s.empty},`加载失败`):t.steps.length===0?(0,r.createElement)(`div`,{style:s.empty},`(空步骤)`):t.steps.map((t,n)=>(0,r.createElement)(`div`,{key:`${e.name}-${n}`,style:s.stepLine,title:`${t.tool} ${JSON.stringify(t.args??{})}`},(0,r.createElement)(`span`,{style:s.meta},`#${n}`),(0,r.createElement)(`span`,{style:s.toolName},t.tool),(0,r.createElement)(`span`,{style:s.stepArgs},JSON.stringify(t.args??{}))))))}),d!==null&&(0,r.createElement)(`div`,{style:s.heading},`提示:agent 在对话中用 browser_record save <name> 创建录制。`))}let d=[`slots`];function f(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},u)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},l))}return n.apply=f,n.inject=d,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={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},a={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 s(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function c(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(){let e=o(),t=e?a:i,[n,c]=(0,r.useState)(`/browser`),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[p,m]=(0,r.useState)(null),[h,g]=(0,r.useState)([]),[_,v]=(0,r.useState)([]),[y,b]=(0,r.useState)(Date.now()),[x,S]=(0,r.useState)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(!1);(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&c(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0,t=()=>{fetch(`${n}/status`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&(m(t),t?.open&&b(Date.now()))}).catch(()=>{e&&m(null)}),fetch(`${n}/log`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&t&&Array.isArray(t.calls)&&g(t.calls.slice(0,8))}).catch(()=>{}),fetch(`${n}/runs/latest`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&S(t?.run??null)}).catch(()=>{})};t();let r=setInterval(t,3e3);return()=>{e=!1,clearInterval(r)}},[n]),(0,r.useEffect)(()=>{let e=!0,t=0,r,i=()=>{fetch(`${n}/confirm`,{cache:`no-store`}).then(n=>{if(!e)return null;if(!n.ok)throw Error(`HTTP ${n.status}`);return t=0,n.json()}).then(t=>{e&&w(t?.confirm??null)}).catch(()=>{}).finally(()=>{e&&(t++,r=setTimeout(i,t>=5?15e3:1e3))})};return i(),()=>{e=!1,r!==void 0&&clearTimeout(r)}},[n]);let D=e=>{let t=C?.id;t&&(w(null),fetch(`${n}/confirm`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,decision:e})}).catch(()=>{}))};(0,r.useEffect)(()=>{if(!d)return;let e=!0,t=()=>{fetch(`${n}/history`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&t&&Array.isArray(t.history)&&v(t.history)}).catch(()=>{}),b(Date.now())};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[d,n]),(0,r.useEffect)(()=>{if(!d)return;let e=e=>{e.key===`Escape`&&f(!1)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[d]);let O=`${n}/screenshot?v=${y}`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>u(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.25)`,pointerEvents:`auto`,userSelect:`none`}},`🌐`),l&&(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`66px`,width:`320px`,maxHeight:`72vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${t.border}`,borderRadius:`12px`,background:e?`#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:p?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:t.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:t.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},p===null?`未启用`:p.open?`${p.tabs??1} 标签 · ${(p.url||``).slice(0,20)}`:`未打开`)),C?(0,r.createElement)(`div`,{style:{padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #ef4444`,background:e?`rgba(239, 68, 68, 0.12)`:`rgba(239, 68, 68, 0.07)`,color:e?`#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`}},C.reason,C.tool?` (${C.tool})`:``),(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,marginTop:`6px`}},(0,r.createElement)(`button`,{onClick:()=>D(`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:()=>D(`denied`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#dc2626`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`拒绝`))):null,p?.pause?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #f59e0b`,background:e?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:e?`#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`}},`等待人工操作: ${p.pause.instruction}`)):null,p?.open?(0,r.createElement)(`img`,{src:O,alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:()=>f(!0),style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${t.border}`,cursor:`pointer`,background:e?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:e?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),h.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${t.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${h.length} 条)`),h.map((e,n)=>(0,r.createElement)(`div`,{key:n,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:t.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:t.tertiary,fontSize:`11px`,flex:`none`}},s(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}`:``}`)))),x!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${t.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:t.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,cursor:`pointer`},title:`runId=${x.runId} · 点击查看报告`,onClick:()=>E(!0)},(0,r.createElement)(`span`,{style:{flex:`none`,color:t.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:x.failed===0?`#10b981`:`#ef4444`}},x.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${x.source}${x.row===void 0?``:` 行${x.row}`} ${x.passed}/${x.total}${x.healedCount>0?` · 自愈${x.healedCount}`:``} · ${new Date(x.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`))),d&&(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&f(!1)},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 ${t.border}`,borderRadius:`12px`,background:e?`#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:t.primary,fontSize:`14px`,fontWeight:600}},p?.open?`${p.title||``} (${p.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:()=>f(!1),style:{width:`28px`,height:`28px`,border:`1px solid ${t.border}`,borderRadius:`6px`,background:`transparent`,color:t.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),p?.open?(0,r.createElement)(`img`,{src:O,alt:`浏览器大图`,style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:`1px solid ${t.border}`,background:e?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),_.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${t.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${_.length} 条,点击在新标签打开)`),_.map((e,n)=>(0,r.createElement)(`a`,{key:n,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:t.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:t.tertiary,flex:`none`}},s(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url)))))),T&&(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&E(!1)},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 ${t.border}`,borderRadius:`12px`,background:e?`#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,color:t.primary,fontSize:`14px`,fontWeight:600}},`回归报告${x===null?``:` — ${x.source}`}(运行趋势 / 失败明细 / 自愈清单)`),(0,r.createElement)(`button`,{type:`button`,onClick:()=>E(!1),style:{width:`28px`,height:`28px`,border:`1px solid ${t.border}`,borderRadius:`6px`,background:`transparent`,color:t.secondary,cursor:`pointer`}},`✕`)),(0,r.createElement)(`iframe`,{src:`${n}/runs/report`,title:`回归报告`,style:{flex:1,width:`100%`,border:`1px solid ${t.border}`,borderRadius:`8px`,background:`#fff`}}))))}function u(e){let t=o(),n=t?a:i,s=c(n),[l,u]=(0,r.useState)(`/browser`),[d,f]=(0,r.useState)(null),[p,m]=(0,r.useState)(``),[h,g]=(0,r.useState)({});(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&u(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0;return f(null),m(``),fetch(`${l}/recordings`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&f(t.recordings??[])}).catch(t=>{e&&m(t instanceof Error?t.message:String(t))}),()=>{e=!1}},[l]);let _=e=>{g(t=>{let n=t[e];if(n!==void 0&&n!==`loading`){let n={...t};return delete n[e],n}return fetch(`${l}/recordings/detail?name=${encodeURIComponent(e)}`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>g(n=>({...n,[e]:t}))).catch(()=>g(t=>({...t,[e]:`failed`}))),{...t,[e]:`loading`}})},v=e=>{fetch(`${l}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:e})}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(()=>{f(t=>t===null?t:t.filter(t=>t.name!==e)),g(t=>{let n={...t};return delete n[e],n})}).catch(()=>{})};return(0,r.createElement)(`div`,{style:s.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:`38px`,height:`38px`,borderRadius:`10px`,display:`flex`,alignItems:`center`,justifyContent:`center`,background:t?`#222938`:`#eef2f6`,border:`1px solid ${n.border}`,fontSize:`20px`,flexShrink:0}},`🌐`),(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}},`共享 Playwright 浏览器自动化、测试原语、操作轨迹与回归录制管理`))),(0,r.createElement)(`div`,{style:s.heading},`已保存的操作录制 (agent 在对话中用 browser_record save <name> 创建,存储于 $DSH_HOME/.dsh-browser/recordings)`),p?(0,r.createElement)(`div`,{style:s.empty},`加载失败: ${p}`):d===null?(0,r.createElement)(`div`,{style:s.empty},`加载中…`):d.length===0?(0,r.createElement)(`div`,{style:s.empty},`暂无录制。让 agent 跑完一轮流程后 browser_record save <name> 创建。`):d.map(e=>{let t=h[e.name];return(0,r.createElement)(`div`,{key:e.name,style:s.itemBox},(0,r.createElement)(`div`,{style:s.row},(0,r.createElement)(`span`,{style:s.label,title:`点击展开步骤`,onClick:()=>_(e.name)},e.name),(0,r.createElement)(`span`,{style:s.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleString()}`),(0,r.createElement)(`button`,{style:s.btn,onClick:()=>_(e.name)},t===void 0?`展开`:`收起`),(0,r.createElement)(`button`,{style:s.delBtn,onClick:()=>v(e.name)},`删除`)),t===void 0?null:t===`loading`?(0,r.createElement)(`div`,{style:s.empty},`加载中…`):(0,r.createElement)(`div`,{style:s.stepsBox},t===`failed`?(0,r.createElement)(`div`,{style:s.empty},`加载失败`):t.steps.length===0?(0,r.createElement)(`div`,{style:s.empty},`(空步骤)`):t.steps.map((t,n)=>(0,r.createElement)(`div`,{key:`${e.name}-${n}`,style:s.stepLine,title:`${t.tool} ${JSON.stringify(t.args??{})}`},(0,r.createElement)(`span`,{style:s.meta},`#${n}`),(0,r.createElement)(`span`,{style:s.toolName},t.tool),(0,r.createElement)(`span`,{style:s.stepArgs},JSON.stringify(t.args??{}))))))}),d!==null&&(0,r.createElement)(`div`,{style:s.heading},`提示:agent 在对话中用 browser_record save <name> 创建录制。`))}let d=[`slots`];function f(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},u)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},l))}return n.apply=f,n.inject=d,t.exports}});
2
2
  //# sourceMappingURL=client.js.map
package/lib/index.js CHANGED
@@ -904,6 +904,33 @@ function deleteRecordingFile(name) {
904
904
  return false;
905
905
  }
906
906
  }
907
+ /**
908
+ * 磁盘热重载:录制文件被外部编辑(手修 JSON)后,同会话内 browser_replay 仍用内存旧缓存。
909
+ * 比较文件 mtime 与内存 savedAt(留 2s 余量容忍写入延迟),更新过则重新加载进内存表。
910
+ * 返回是否发生了重载;文件不存在或损坏时静默保留内存版。
911
+ */
912
+ function reloadRecordingIfChanged(name) {
913
+ const mem = savedRecordings.get(name);
914
+ const file = join(recordingsDir(), `${safeName(name)}.json`);
915
+ let mtime;
916
+ try {
917
+ mtime = statSync(file).mtimeMs;
918
+ } catch {
919
+ return false;
920
+ }
921
+ if (mem !== void 0 && mtime <= mem.savedAt + 2e3) return false;
922
+ try {
923
+ const data = JSON.parse(readFileSync(file, "utf8"));
924
+ if (data !== null && typeof data === "object" && Array.isArray(data.steps)) {
925
+ savedRecordings.set(name, {
926
+ steps: data.steps,
927
+ savedAt: mtime
928
+ });
929
+ return true;
930
+ }
931
+ } catch {}
932
+ return false;
933
+ }
907
934
  /** 录制列表(面板 / browser_record list 共用)。 */
908
935
  function listRecordings() {
909
936
  return [...savedRecordings.entries()].map(([name, r]) => ({
@@ -1951,7 +1978,21 @@ async function targetLocator(page, args) {
1951
1978
  }
1952
1979
  async function clickTool(config, args) {
1953
1980
  const page = await getPage(config);
1954
- await (await targetLocator(page, args)).click({ timeout: args.timeoutMs ?? config.timeoutMs });
1981
+ const target = await targetLocator(page, args);
1982
+ const label = String(args.ref ?? args.selector ?? "");
1983
+ try {
1984
+ await target.click({ timeout: args.timeoutMs ?? config.timeoutMs });
1985
+ } catch (e) {
1986
+ const msg = e instanceof Error ? e.message : String(e);
1987
+ let diag = "";
1988
+ try {
1989
+ if (await target.count() === 0) diag = "元素未匹配(selector 可能写错或元素未渲染)";
1990
+ else diag = await target.first().isVisible() ? "元素可见但不可交互(被遮挡/禁用?);若页面刚发生跳转,点击可能实际已生效" : "元素存在但不可见";
1991
+ } catch {
1992
+ diag = "元素已从页面移除(若页面刚发生跳转,点击可能实际已生效)";
1993
+ }
1994
+ throw new Error(`click 失败: ${label} — ${diag};原始错误: ${msg}`);
1995
+ }
1955
1996
  await page.waitForLoadState("domcontentloaded").catch(() => {});
1956
1997
  return {
1957
1998
  clicked: String(args.ref ?? args.selector),
@@ -2015,7 +2056,19 @@ async function selectTool(config, args) {
2015
2056
  const options = await target.locator("option").allTextContents();
2016
2057
  throw new Error(`原生下拉无选项 "${want}";可用选项: ${options.map((t) => t.trim()).join(" / ")}`);
2017
2058
  }
2018
- await target.click({ timeout });
2059
+ const clickTimeout = Math.min(timeout, 1e4);
2060
+ try {
2061
+ await target.click({ timeout: clickTimeout });
2062
+ } catch (e0) {
2063
+ try {
2064
+ await target.click({
2065
+ timeout: 2e3,
2066
+ force: true
2067
+ });
2068
+ } catch {
2069
+ throw new Error(`下拉触发框点击失败(元素未出现/被遮挡/不可交互): ${label};原始错误: ${e0 instanceof Error ? e0.message : String(e0)}。可尝试 selector 指向上层触发容器(如 .ant-select),或用 browser_snapshot { mode: "aria" } 拿 ref`);
2070
+ }
2071
+ }
2019
2072
  const options = page.locator(DROPDOWN_OPTION_SELECTOR);
2020
2073
  await options.first().waitFor({
2021
2074
  state: "visible",
@@ -2035,9 +2088,28 @@ async function selectTool(config, args) {
2035
2088
  ...await pageIdentity(page)
2036
2089
  };
2037
2090
  }
2091
+ /**
2092
+ * 防御性解码 eval 脚本:模型/人工偶发把脚本 JSON.stringify 一次再传
2093
+ * (脚本以 \" 开头、换行变字面 \\n),回放/录制链路不纠错会导致语法错误。
2094
+ * 识别条件:字符串以引号包裹且含反斜杠转义,且 JSON.parse 结果仍是字符串。
2095
+ * 无法解析或语义不变场景原样返回(裸字符串字面量如 "hello" 不含转义,不受影响)。
2096
+ */
2097
+ function unwrapDoubleEncodedScript(script) {
2098
+ if (script.length >= 2 && script.startsWith("\"") && script.endsWith("\"") && script.includes("\\")) try {
2099
+ const parsed = JSON.parse(script);
2100
+ if (typeof parsed === "string" && parsed !== script) return {
2101
+ script: parsed,
2102
+ unwrapped: true
2103
+ };
2104
+ } catch {}
2105
+ return {
2106
+ script,
2107
+ unwrapped: false
2108
+ };
2109
+ }
2038
2110
  async function evalTool(config, args) {
2039
2111
  const page = await getPage(config);
2040
- const script = String(args.script ?? "");
2112
+ const { script, unwrapped } = unwrapDoubleEncodedScript(String(args.script ?? ""));
2041
2113
  const wrapped = /[\n;]|^\s*return\b/.test(script) ? `(async () => {\n${script}\n})()` : `(${script})`;
2042
2114
  const raw = await page.evaluate(wrapped);
2043
2115
  let result;
@@ -2046,7 +2118,10 @@ async function evalTool(config, args) {
2046
2118
  } catch {
2047
2119
  result = raw === void 0 ? null : String(raw);
2048
2120
  }
2049
- return { result };
2121
+ return {
2122
+ result,
2123
+ ...unwrapped ? { scriptUnwrapped: true } : {}
2124
+ };
2050
2125
  }
2051
2126
  async function screenshotTool(config, args, exec) {
2052
2127
  const page = await getPage(config);
@@ -3342,15 +3417,27 @@ async function recordTool(args) {
3342
3417
  if (!name) throw new Error("save 需要 name");
3343
3418
  const recorded = lastRecordingOf();
3344
3419
  const source = recorded !== null ? "recording" : "recent-log";
3345
- const rawSteps = recorded !== null ? recorded.map((c) => ({
3346
- tool: c.tool,
3347
- args: c.args ?? {},
3348
- ...c.meta !== void 0 ? { meta: c.meta } : {}
3349
- })) : testLogOf().reverse().slice(-100).map((c) => ({
3350
- tool: c.tool,
3351
- args: c.args ?? {},
3352
- ...c.meta !== void 0 ? { meta: c.meta } : {}
3353
- }));
3420
+ const calls = recorded !== null ? recorded : testLogOf().reverse().slice(-100);
3421
+ const includeFailed = args.includeFailed === true;
3422
+ const failedCount = includeFailed ? 0 : calls.filter((c) => c.ok === false).length;
3423
+ const rawSteps = (includeFailed ? calls : calls.filter((c) => c.ok !== false)).map((c) => {
3424
+ if (c.tool === "browser_eval" && typeof c.args?.script === "string") {
3425
+ const { script } = unwrapDoubleEncodedScript(c.args.script);
3426
+ if (script !== c.args.script) return {
3427
+ tool: c.tool,
3428
+ args: {
3429
+ ...c.args,
3430
+ script
3431
+ },
3432
+ ...c.meta !== void 0 ? { meta: c.meta } : {}
3433
+ };
3434
+ }
3435
+ return {
3436
+ tool: c.tool,
3437
+ args: c.args ?? {},
3438
+ ...c.meta !== void 0 ? { meta: c.meta } : {}
3439
+ };
3440
+ });
3354
3441
  const steps = args.stripReads === true ? rawSteps.filter((s) => !isReadonlyStep(s)) : rawSteps;
3355
3442
  const stripped = rawSteps.length - steps.length;
3356
3443
  let varsApplied = 0;
@@ -3379,6 +3466,7 @@ async function recordTool(args) {
3379
3466
  source,
3380
3467
  file,
3381
3468
  ...stripped > 0 ? { stripped } : {},
3469
+ ...failedCount > 0 ? { skippedFailed: failedCount } : {},
3382
3470
  ...varsApplied > 0 ? { varsApplied } : {},
3383
3471
  ...warnings.length > 0 ? { warnings } : {},
3384
3472
  hint: "已持久化到磁盘,重启 DSH 后仍可回放"
@@ -3760,10 +3848,12 @@ async function replayTool(args, impls, config, exec) {
3760
3848
  let steps = [];
3761
3849
  let source = "steps";
3762
3850
  if (typeof args.name === "string" && args.name) {
3851
+ const reloaded = reloadRecordingIfChanged(args.name);
3763
3852
  const saved = savedRecordings.get(args.name);
3764
3853
  if (saved === void 0) throw new Error(`未找到录制: ${args.name}`);
3765
3854
  steps = saved.steps;
3766
3855
  source = args.name;
3856
+ if (reloaded) source = `${args.name}(已从磁盘重载)`;
3767
3857
  } else if (Array.isArray(args.steps)) steps = args.steps;
3768
3858
  else throw new Error("replay 需要 name(已保存录制)或 steps(步骤数组)");
3769
3859
  if (steps.length === 0) throw new Error("步骤为空");
@@ -4304,7 +4394,13 @@ function registerTools(ctx, config) {
4304
4394
  outputSchema: {
4305
4395
  type: "object",
4306
4396
  additionalProperties: false,
4307
- properties: { result: {} },
4397
+ properties: {
4398
+ result: {},
4399
+ scriptUnwrapped: {
4400
+ type: "boolean",
4401
+ description: "入参 script 疑似被 JSON 双重序列化(以引号包裹且含 \\n 字面量),已自动解码一层。"
4402
+ }
4403
+ },
4308
4404
  required: ["result"]
4309
4405
  },
4310
4406
  render: (_args, value) => `Result: ${JSON.stringify(value.result)}`,
@@ -4860,7 +4956,7 @@ function registerTools(ctx, config) {
4860
4956
  ]
4861
4957
  },
4862
4958
  render: (_args, value) => value.loggedIn ? `登录完成: ${value.url}` : `等待登录超时,仍在: ${value.url}`,
4863
- timeoutMs: timeout(),
4959
+ timeoutMs: timeout(6e5),
4864
4960
  execute: (args) => waitForLoginTool(config, args)
4865
4961
  }));
4866
4962
  register(definePlainTool({
@@ -5409,6 +5505,10 @@ function registerTools(ctx, config) {
5409
5505
  type: "boolean",
5410
5506
  description: "save 时剔除只读步骤(snapshot/elements/eval/network/status/cookies list),默认 false。"
5411
5507
  },
5508
+ includeFailed: {
5509
+ type: "boolean",
5510
+ description: "save 时保留失败的实验步骤(默认剔除:超时/无效 selector 的步骤回放必然再次失败并卡住回放)。"
5511
+ },
5412
5512
  vars: {
5413
5513
  type: "object",
5414
5514
  description: "save 时参数化:{ 变量名: \"#步骤号.args.字段\" }(步骤号 1 起),该字段值替换为 {{变量名}} 占位符;replay 时用 vars 传入真实值。"
@@ -5423,6 +5523,10 @@ function registerTools(ctx, config) {
5423
5523
  recording: { type: "boolean" },
5424
5524
  steps: { type: "integer" },
5425
5525
  stripped: { type: "integer" },
5526
+ skippedFailed: {
5527
+ type: "integer",
5528
+ description: "save 时默认剔除的失败实验步骤数。"
5529
+ },
5426
5530
  varsApplied: { type: "integer" },
5427
5531
  saved: { type: "string" },
5428
5532
  source: { type: "string" },
@@ -5527,7 +5631,7 @@ function registerTools(ctx, config) {
5527
5631
  render: (_args, value) => {
5528
5632
  return value.runs !== void 0 ? `${value.summary}\n${value.runs.map((r) => `${(r.failed ?? 0) === 0 ? "✓" : "✗"} 行${r.row}${r.error ? ` — ${String(r.error).slice(0, 100)}` : ` ${r.passed}/${r.total}`}`).join("\n")}` : `${value.summary}\n${(value.results ?? []).map((r) => `${r.ok ? "✓" : "✗"} #${r.index} ${r.tool}${r.healed ? ` [自愈:${r.via}]` : ""}${r.error ? ` — ${String(r.error).slice(0, 120)}` : ""}`).join("\n")}`;
5529
5633
  },
5530
- timeoutMs: timeout(),
5634
+ timeoutMs: timeout(6e5),
5531
5635
  execute: (args, exec) => replayTool(args, stepImpls, config, exec)
5532
5636
  }));
5533
5637
  register(definePlainTool({
@@ -5574,7 +5678,7 @@ function registerTools(ctx, config) {
5574
5678
  ]
5575
5679
  },
5576
5680
  render: (_args, value) => `${value.summary}\n${(value.results ?? []).map((r) => `${r.ok ? "✓" : "✗"} #${r.index} ${r.tool}${r.error ? ` — ${String(r.error).slice(0, 120)}` : ""}`).join("\n")}`,
5577
- timeoutMs: timeout(),
5681
+ timeoutMs: timeout(6e5),
5578
5682
  execute: (args) => batchTool(args, stepImpls)
5579
5683
  }));
5580
5684
  register(definePlainTool({
@@ -9,6 +9,12 @@ export declare function loadRecordings(): number;
9
9
  export declare function saveRecordingFile(name: string, steps: ReplayStep[]): string;
10
10
  /** 删除磁盘上的录制文件;返回是否存在并删除。 */
11
11
  export declare function deleteRecordingFile(name: string): boolean;
12
+ /**
13
+ * 磁盘热重载:录制文件被外部编辑(手修 JSON)后,同会话内 browser_replay 仍用内存旧缓存。
14
+ * 比较文件 mtime 与内存 savedAt(留 2s 余量容忍写入延迟),更新过则重新加载进内存表。
15
+ * 返回是否发生了重载;文件不存在或损坏时静默保留内存版。
16
+ */
17
+ export declare function reloadRecordingIfChanged(name: string): boolean;
12
18
  /** 录制列表(面板 / browser_record list 共用)。 */
13
19
  export declare function listRecordings(): Array<{
14
20
  name: string;
@@ -155,7 +155,18 @@ export declare function selectTool(config: BrowserConfig, args: Record<string, a
155
155
  selected: string;
156
156
  selector: string;
157
157
  }>;
158
+ /**
159
+ * 防御性解码 eval 脚本:模型/人工偶发把脚本 JSON.stringify 一次再传
160
+ * (脚本以 \" 开头、换行变字面 \\n),回放/录制链路不纠错会导致语法错误。
161
+ * 识别条件:字符串以引号包裹且含反斜杠转义,且 JSON.parse 结果仍是字符串。
162
+ * 无法解析或语义不变场景原样返回(裸字符串字面量如 "hello" 不含转义,不受影响)。
163
+ */
164
+ export declare function unwrapDoubleEncodedScript(script: string): {
165
+ script: string;
166
+ unwrapped: boolean;
167
+ };
158
168
  export declare function evalTool(config: BrowserConfig, args: Record<string, any>): Promise<{
169
+ scriptUnwrapped?: boolean | undefined;
159
170
  result: unknown;
160
171
  }>;
161
172
  export declare function screenshotTool(config: BrowserConfig, args: Record<string, any>, exec?: ToolRunContext): Promise<{
@@ -566,6 +577,7 @@ export declare function recordTool(args: Record<string, any>): Promise<{
566
577
  hint: string;
567
578
  warnings?: string[] | undefined;
568
579
  varsApplied?: number | undefined;
580
+ skippedFailed?: number | undefined;
569
581
  stripped?: number | undefined;
570
582
  saved: string;
571
583
  steps: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-view",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "DSH 插件:共享 Playwright 浏览器自动化(36 个 browser_* 工具,含语义元素查找、批处理、视觉回归、选择器自愈、参数化回放、结构化抽取、回归报告与隔离 context)+ 人机协作(人工接管等待、敏感操作审批、人机混合录制、面板双向化:操作预告高亮/内嵌报告)+ 实时画面面板与录制管理。仅依赖 playwright-core,不依赖任何 @deepseek-ai 运行时包。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",