dsh-browser-view 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/client.js +1 -1
- package/lib/index.js +82 -112
- package/lib/types/browser/tools.d.ts +12 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ DSH 插件:共享 Playwright 浏览器自动化 + 原生界面实时画面面板
|
|
|
5
5
|
| 模块 | 做什么 |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| 🧭 **浏览器自动化** | 共享 Playwright 浏览器:多标签、多 profile、隔离 context、下载/上传、Cookie、表单回放,36 个 `browser_*` 工具 |
|
|
8
|
-
| 🖥️ **实时画面** | 注入 DSH 界面右下角悬浮面板:浏览器状态 + 实时缩略图,点开大屏模态框(2 秒轮询画面 + 访问历史);**双向协作**——agent
|
|
8
|
+
| 🖥️ **实时画面** | 注入 DSH 界面右下角悬浮面板:浏览器状态 + 实时缩略图,点开大屏模态框(2 秒轮询画面 + 访问历史);**双向协作**——agent 等待人工操作/敏感审批时显示交互横幅、执行点击/输入前目标元素琥珀色高亮预告,「最近回归」行点击直接内嵌查看 HTML 回归报告 |
|
|
9
9
|
| ✅ **回归测试闭环** | 录制回放 + 选择器自愈 + 参数化/数据驱动 + 视觉回归 + 运行历史与报告导出 |
|
|
10
10
|
|
|
11
11
|
设计原则:能做的一次性操作交给 agent 用 bash 完成,插件只做 bash 做不到的常驻浏览器自动化。唯一第三方运行时依赖是 `playwright-core`(复用系统已装的 Edge/Chrome,无需下载 Chromium),不依赖任何 `@deepseek-ai` 运行时包。
|
package/lib/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i={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),[D,O]=(0,r.useState)(!1),[k,A]=(0,r.useState)(null);(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 j=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(()=>{}))},M=()=>{if(D)return;A(null),O(!0),fetch(`${n}/pick`,{method:`POST`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).catch(()=>{});let e=Date.now()+6e4,t=setInterval(()=>{if(Date.now()>e){clearInterval(t),O(!1);return}fetch(`${n}/pick`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(e=>{e?.pick&&(clearInterval(t),O(!1),A(e.pick))}).catch(()=>{})},500)},N=()=>{let e=k?.selector;e&&navigator.clipboard?.writeText(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 P=`${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:()=>j(`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:()=>j(`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:P,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`,title:D?`拾取中:在浏览器窗口点击任意元素`:`点选元素生成 selector`,onClick:M,style:{flex:`none`,padding:`2px 10px`,border:`1px solid ${D?`#f59e0b`:t.border}`,borderRadius:`6px`,background:D?`rgba(245, 158, 11, 0.12)`:`transparent`,color:D?`#f59e0b`:t.secondary,cursor:`pointer`,fontSize:`12px`}},D?`◎ 拾取中…`:`🎯 点选元素`),(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`}},`✕`)),k!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #10b981`,background:e?`rgba(16, 185, 129, 0.1)`:`rgba(16, 185, 129, 0.06)`,fontSize:`12px`}},(0,r.createElement)(`div`,{style:{flex:1,minWidth:0}},(0,r.createElement)(`div`,{style:{color:t.primary,fontFamily:`monospace`,wordBreak:`break-all`}},k.selector),(0,r.createElement)(`div`,{style:{color:t.tertiary,marginTop:`2px`}},`${k.tag}${k.role?` · role=${k.role}`:``}${k.name?` · "${k.name}"`:``} — 可作为 browser_click/type/select 的 selector 告诉 agent`)),(0,r.createElement)(`button`,{type:`button`,onClick:N,style:{flex:`none`,padding:`4px 10px`,border:`none`,borderRadius:`6px`,background:`#10b981`,color:`#fff`,cursor:`pointer`,fontSize:`12px`,fontWeight:600}},`复制 selector`)),p?.open?(0,r.createElement)(`img`,{src:P,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=()=>{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}});
|
|
2
2
|
//# sourceMappingURL=client.js.map
|
package/lib/index.js
CHANGED
|
@@ -2035,9 +2035,28 @@ async function selectTool(config, args) {
|
|
|
2035
2035
|
...await pageIdentity(page)
|
|
2036
2036
|
};
|
|
2037
2037
|
}
|
|
2038
|
+
/**
|
|
2039
|
+
* 防御性解码 eval 脚本:模型/人工偶发把脚本 JSON.stringify 一次再传
|
|
2040
|
+
* (脚本以 \" 开头、换行变字面 \\n),回放/录制链路不纠错会导致语法错误。
|
|
2041
|
+
* 识别条件:字符串以引号包裹且含反斜杠转义,且 JSON.parse 结果仍是字符串。
|
|
2042
|
+
* 无法解析或语义不变场景原样返回(裸字符串字面量如 "hello" 不含转义,不受影响)。
|
|
2043
|
+
*/
|
|
2044
|
+
function unwrapDoubleEncodedScript(script) {
|
|
2045
|
+
if (script.length >= 2 && script.startsWith("\"") && script.endsWith("\"") && script.includes("\\")) try {
|
|
2046
|
+
const parsed = JSON.parse(script);
|
|
2047
|
+
if (typeof parsed === "string" && parsed !== script) return {
|
|
2048
|
+
script: parsed,
|
|
2049
|
+
unwrapped: true
|
|
2050
|
+
};
|
|
2051
|
+
} catch {}
|
|
2052
|
+
return {
|
|
2053
|
+
script,
|
|
2054
|
+
unwrapped: false
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2038
2057
|
async function evalTool(config, args) {
|
|
2039
2058
|
const page = await getPage(config);
|
|
2040
|
-
const script = String(args.script ?? "");
|
|
2059
|
+
const { script, unwrapped } = unwrapDoubleEncodedScript(String(args.script ?? ""));
|
|
2041
2060
|
const wrapped = /[\n;]|^\s*return\b/.test(script) ? `(async () => {\n${script}\n})()` : `(${script})`;
|
|
2042
2061
|
const raw = await page.evaluate(wrapped);
|
|
2043
2062
|
let result;
|
|
@@ -2046,7 +2065,10 @@ async function evalTool(config, args) {
|
|
|
2046
2065
|
} catch {
|
|
2047
2066
|
result = raw === void 0 ? null : String(raw);
|
|
2048
2067
|
}
|
|
2049
|
-
return {
|
|
2068
|
+
return {
|
|
2069
|
+
result,
|
|
2070
|
+
...unwrapped ? { scriptUnwrapped: true } : {}
|
|
2071
|
+
};
|
|
2050
2072
|
}
|
|
2051
2073
|
async function screenshotTool(config, args, exec) {
|
|
2052
2074
|
const page = await getPage(config);
|
|
@@ -2260,69 +2282,6 @@ async function contextTool(config, args) {
|
|
|
2260
2282
|
throw new Error(`未知 action: ${action}(list | new | switch | close)`);
|
|
2261
2283
|
}
|
|
2262
2284
|
/**
|
|
2263
|
-
* 页内拾取器(evaluate 注入):一次性 click 捕获 —— 点击任意元素时阻止默认行为,
|
|
2264
|
-
* 计算稳定 CSS 路径 + 元素信息存 window.__dshPickResult(面板轮询读取后自清)。
|
|
2265
|
-
* 与 aria walker 的 cssPath 同一套语义(#id → [data-testid] → nth-of-type 链)。
|
|
2266
|
-
*/
|
|
2267
|
-
function pickElementInjector() {
|
|
2268
|
-
const w = window;
|
|
2269
|
-
if (w.__dshPicking === true) return;
|
|
2270
|
-
w.__dshPicking = true;
|
|
2271
|
-
function cssPath(el) {
|
|
2272
|
-
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
2273
|
-
const testid = el.getAttribute("data-testid") ?? el.getAttribute("data-test-id");
|
|
2274
|
-
if (testid) return `[data-testid="${testid.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"]`;
|
|
2275
|
-
const parts = [];
|
|
2276
|
-
let cur = el;
|
|
2277
|
-
while (cur && cur !== document.body && parts.length < 6) {
|
|
2278
|
-
const tag = cur.tagName.toLowerCase();
|
|
2279
|
-
const parent = cur.parentElement;
|
|
2280
|
-
if (!parent) {
|
|
2281
|
-
parts.unshift(tag);
|
|
2282
|
-
break;
|
|
2283
|
-
}
|
|
2284
|
-
const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
|
|
2285
|
-
const idx = sameTag.indexOf(cur) + 1;
|
|
2286
|
-
parts.unshift(sameTag.length > 1 ? `${tag}:nth-of-type(${idx})` : tag);
|
|
2287
|
-
cur = parent;
|
|
2288
|
-
}
|
|
2289
|
-
return parts.join(" > ");
|
|
2290
|
-
}
|
|
2291
|
-
const handler = (e) => {
|
|
2292
|
-
const el = e.target instanceof Element ? e.target : null;
|
|
2293
|
-
if (el === null) return;
|
|
2294
|
-
e.preventDefault();
|
|
2295
|
-
e.stopPropagation();
|
|
2296
|
-
const aria = el.getAttribute("aria-label");
|
|
2297
|
-
w.__dshPickResult = {
|
|
2298
|
-
selector: cssPath(el),
|
|
2299
|
-
tag: el.tagName.toLowerCase(),
|
|
2300
|
-
id: el.id ?? "",
|
|
2301
|
-
testid: el.getAttribute("data-testid") ?? el.getAttribute("data-test-id") ?? "",
|
|
2302
|
-
role: el.getAttribute("role") ?? "",
|
|
2303
|
-
name: aria && aria.trim() ? aria.trim().slice(0, 60) : (el.innerText || el.textContent || "").trim().slice(0, 60),
|
|
2304
|
-
url: location.href
|
|
2305
|
-
};
|
|
2306
|
-
w.__dshPicking = false;
|
|
2307
|
-
document.removeEventListener("click", handler, true);
|
|
2308
|
-
};
|
|
2309
|
-
document.addEventListener("click", handler, true);
|
|
2310
|
-
}
|
|
2311
|
-
/** 读取并清空拾取结果(ui.ts /pick GET 调用;无结果返回 null)。 */
|
|
2312
|
-
async function takePickResult(page) {
|
|
2313
|
-
try {
|
|
2314
|
-
return await page.evaluate(() => {
|
|
2315
|
-
const w = window;
|
|
2316
|
-
const out = w.__dshPickResult ?? null;
|
|
2317
|
-
w.__dshPickResult = null;
|
|
2318
|
-
w.__dshPicking = false;
|
|
2319
|
-
return out;
|
|
2320
|
-
});
|
|
2321
|
-
} catch {
|
|
2322
|
-
return null;
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
/**
|
|
2326
2285
|
* 操作预告高亮:agent 即将 click/type/select/press 的元素在页面上琥珀色描边
|
|
2327
2286
|
* ~1.5 秒。异步不阻塞(fire-and-forget,任何失败静默),不影响操作时序。
|
|
2328
2287
|
*/
|
|
@@ -3405,15 +3364,27 @@ async function recordTool(args) {
|
|
|
3405
3364
|
if (!name) throw new Error("save 需要 name");
|
|
3406
3365
|
const recorded = lastRecordingOf();
|
|
3407
3366
|
const source = recorded !== null ? "recording" : "recent-log";
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3367
|
+
const calls = recorded !== null ? recorded : testLogOf().reverse().slice(-100);
|
|
3368
|
+
const includeFailed = args.includeFailed === true;
|
|
3369
|
+
const failedCount = includeFailed ? 0 : calls.filter((c) => c.ok === false).length;
|
|
3370
|
+
const rawSteps = (includeFailed ? calls : calls.filter((c) => c.ok !== false)).map((c) => {
|
|
3371
|
+
if (c.tool === "browser_eval" && typeof c.args?.script === "string") {
|
|
3372
|
+
const { script } = unwrapDoubleEncodedScript(c.args.script);
|
|
3373
|
+
if (script !== c.args.script) return {
|
|
3374
|
+
tool: c.tool,
|
|
3375
|
+
args: {
|
|
3376
|
+
...c.args,
|
|
3377
|
+
script
|
|
3378
|
+
},
|
|
3379
|
+
...c.meta !== void 0 ? { meta: c.meta } : {}
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
return {
|
|
3383
|
+
tool: c.tool,
|
|
3384
|
+
args: c.args ?? {},
|
|
3385
|
+
...c.meta !== void 0 ? { meta: c.meta } : {}
|
|
3386
|
+
};
|
|
3387
|
+
});
|
|
3417
3388
|
const steps = args.stripReads === true ? rawSteps.filter((s) => !isReadonlyStep(s)) : rawSteps;
|
|
3418
3389
|
const stripped = rawSteps.length - steps.length;
|
|
3419
3390
|
let varsApplied = 0;
|
|
@@ -3442,6 +3413,7 @@ async function recordTool(args) {
|
|
|
3442
3413
|
source,
|
|
3443
3414
|
file,
|
|
3444
3415
|
...stripped > 0 ? { stripped } : {},
|
|
3416
|
+
...failedCount > 0 ? { skippedFailed: failedCount } : {},
|
|
3445
3417
|
...varsApplied > 0 ? { varsApplied } : {},
|
|
3446
3418
|
...warnings.length > 0 ? { warnings } : {},
|
|
3447
3419
|
hint: "已持久化到磁盘,重启 DSH 后仍可回放"
|
|
@@ -3934,16 +3906,11 @@ function registerTools(ctx, config) {
|
|
|
3934
3906
|
if (tools === void 0) return;
|
|
3935
3907
|
setToolsLogger((m) => ctx.logger?.info?.(m));
|
|
3936
3908
|
const timeout = (ms) => ms ?? config.timeoutMs;
|
|
3937
|
-
/**
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
const s = JSON.stringify(v);
|
|
3943
|
-
out[k] = s && s.length > 300 ? `${s.slice(0, 300)}…(截断)` : v;
|
|
3944
|
-
}
|
|
3945
|
-
return out;
|
|
3946
|
-
}
|
|
3909
|
+
/**
|
|
3910
|
+
* 打点包装:每次工具调用记录操作轨迹(测试报告 / 录制数据源)。
|
|
3911
|
+
* 注意:记录必须保留完整原始 args —— 录制/回放依赖脚本、steps 数组等大字段的
|
|
3912
|
+
* 完整性(截断会让回放得到损坏的 script / 空的 steps)。展示层截断在 ui.ts 做。
|
|
3913
|
+
*/
|
|
3947
3914
|
/** 会触发真实 DOM 事件的 agent 工具:执行前后打点,混合录制时用于区分人手与工具回声。 */
|
|
3948
3915
|
const AGENT_DOM_TOOLS = /* @__PURE__ */ new Set([
|
|
3949
3916
|
"browser_click",
|
|
@@ -3997,7 +3964,6 @@ function registerTools(ctx, config) {
|
|
|
3997
3964
|
const original = def.execute;
|
|
3998
3965
|
def.execute = async (args, exec) => {
|
|
3999
3966
|
const t0 = Date.now();
|
|
4000
|
-
const sanitized = sanitizeArgs(args);
|
|
4001
3967
|
const domTool = AGENT_DOM_TOOLS.has(def.name);
|
|
4002
3968
|
if (domTool) noteAgentAction();
|
|
4003
3969
|
if (PREVIEW_TOOLS.has(def.name)) {
|
|
@@ -4009,7 +3975,7 @@ function registerTools(ctx, config) {
|
|
|
4009
3975
|
if (domTool) noteAgentAction();
|
|
4010
3976
|
const call = recordCall({
|
|
4011
3977
|
tool: def.name,
|
|
4012
|
-
args
|
|
3978
|
+
args,
|
|
4013
3979
|
ok: true,
|
|
4014
3980
|
ms: Date.now() - t0,
|
|
4015
3981
|
ts: Date.now()
|
|
@@ -4025,7 +3991,7 @@ function registerTools(ctx, config) {
|
|
|
4025
3991
|
} catch (error) {
|
|
4026
3992
|
recordCall({
|
|
4027
3993
|
tool: def.name,
|
|
4028
|
-
args
|
|
3994
|
+
args,
|
|
4029
3995
|
ok: false,
|
|
4030
3996
|
ms: Date.now() - t0,
|
|
4031
3997
|
ts: Date.now(),
|
|
@@ -4373,7 +4339,13 @@ function registerTools(ctx, config) {
|
|
|
4373
4339
|
outputSchema: {
|
|
4374
4340
|
type: "object",
|
|
4375
4341
|
additionalProperties: false,
|
|
4376
|
-
properties: {
|
|
4342
|
+
properties: {
|
|
4343
|
+
result: {},
|
|
4344
|
+
scriptUnwrapped: {
|
|
4345
|
+
type: "boolean",
|
|
4346
|
+
description: "入参 script 疑似被 JSON 双重序列化(以引号包裹且含 \\n 字面量),已自动解码一层。"
|
|
4347
|
+
}
|
|
4348
|
+
},
|
|
4377
4349
|
required: ["result"]
|
|
4378
4350
|
},
|
|
4379
4351
|
render: (_args, value) => `Result: ${JSON.stringify(value.result)}`,
|
|
@@ -5478,6 +5450,10 @@ function registerTools(ctx, config) {
|
|
|
5478
5450
|
type: "boolean",
|
|
5479
5451
|
description: "save 时剔除只读步骤(snapshot/elements/eval/network/status/cookies list),默认 false。"
|
|
5480
5452
|
},
|
|
5453
|
+
includeFailed: {
|
|
5454
|
+
type: "boolean",
|
|
5455
|
+
description: "save 时保留失败的实验步骤(默认剔除:超时/无效 selector 的步骤回放必然再次失败并卡住回放)。"
|
|
5456
|
+
},
|
|
5481
5457
|
vars: {
|
|
5482
5458
|
type: "object",
|
|
5483
5459
|
description: "save 时参数化:{ 变量名: \"#步骤号.args.字段\" }(步骤号 1 起),该字段值替换为 {{变量名}} 占位符;replay 时用 vars 传入真实值。"
|
|
@@ -5492,6 +5468,10 @@ function registerTools(ctx, config) {
|
|
|
5492
5468
|
recording: { type: "boolean" },
|
|
5493
5469
|
steps: { type: "integer" },
|
|
5494
5470
|
stripped: { type: "integer" },
|
|
5471
|
+
skippedFailed: {
|
|
5472
|
+
type: "integer",
|
|
5473
|
+
description: "save 时默认剔除的失败实验步骤数。"
|
|
5474
|
+
},
|
|
5495
5475
|
varsApplied: { type: "integer" },
|
|
5496
5476
|
saved: { type: "string" },
|
|
5497
5477
|
source: { type: "string" },
|
|
@@ -5787,6 +5767,16 @@ function registerTools(ctx, config) {
|
|
|
5787
5767
|
}
|
|
5788
5768
|
//#endregion
|
|
5789
5769
|
//#region lib/types/browser/ui.js
|
|
5770
|
+
/** 展示层参数截断:单字段 JSON 超过 300 字符截断(仅用于面板轨迹显示,不进录制)。 */
|
|
5771
|
+
function truncateForDisplay(args) {
|
|
5772
|
+
if (typeof args !== "object" || args === null) return args;
|
|
5773
|
+
const out = {};
|
|
5774
|
+
for (const [k, v] of Object.entries(args)) {
|
|
5775
|
+
const s = JSON.stringify(v);
|
|
5776
|
+
out[k] = s && s.length > 300 ? `${s.slice(0, 300)}…(截断)` : v;
|
|
5777
|
+
}
|
|
5778
|
+
return out;
|
|
5779
|
+
}
|
|
5790
5780
|
function registerBrowserApi(ctx, config, base) {
|
|
5791
5781
|
const webServer = ctx.get("webServer");
|
|
5792
5782
|
if (webServer === void 0) return;
|
|
@@ -5813,7 +5803,10 @@ function registerBrowserApi(ctx, config, base) {
|
|
|
5813
5803
|
if (path === `${base}/log`) {
|
|
5814
5804
|
sendJson(res, 200, {
|
|
5815
5805
|
profile: currentProfile,
|
|
5816
|
-
calls: testLogOf().slice(0, 50)
|
|
5806
|
+
calls: testLogOf().slice(0, 50).map((c) => ({
|
|
5807
|
+
...c,
|
|
5808
|
+
args: truncateForDisplay(c.args)
|
|
5809
|
+
}))
|
|
5817
5810
|
});
|
|
5818
5811
|
return;
|
|
5819
5812
|
}
|
|
@@ -5836,29 +5829,6 @@ function registerBrowserApi(ctx, config, base) {
|
|
|
5836
5829
|
res.end(html);
|
|
5837
5830
|
return;
|
|
5838
5831
|
}
|
|
5839
|
-
if (path === `${base}/pick`) {
|
|
5840
|
-
if ((req.method ?? "GET") === "POST") {
|
|
5841
|
-
const page = activePage();
|
|
5842
|
-
if (page === null || page.isClosed()) {
|
|
5843
|
-
sendText(res, 409, "browser not open");
|
|
5844
|
-
return;
|
|
5845
|
-
}
|
|
5846
|
-
try {
|
|
5847
|
-
await page.evaluate(pickElementInjector);
|
|
5848
|
-
sendJson(res, 200, { started: true });
|
|
5849
|
-
} catch (error) {
|
|
5850
|
-
sendText(res, 500, `pick start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5851
|
-
}
|
|
5852
|
-
return;
|
|
5853
|
-
}
|
|
5854
|
-
const page = activePage();
|
|
5855
|
-
if (page === null || page.isClosed()) {
|
|
5856
|
-
sendJson(res, 200, { pick: null });
|
|
5857
|
-
return;
|
|
5858
|
-
}
|
|
5859
|
-
sendJson(res, 200, { pick: await takePickResult(page) });
|
|
5860
|
-
return;
|
|
5861
|
-
}
|
|
5862
5832
|
if (path === `${base}/recordings`) {
|
|
5863
5833
|
if ((req.method ?? "GET") === "POST") {
|
|
5864
5834
|
const body = await readJsonBody(req).catch(() => void 0);
|
|
@@ -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<{
|
|
@@ -251,14 +262,6 @@ export declare function contextTool(config: BrowserConfig, args: Record<string,
|
|
|
251
262
|
contexts?: undefined;
|
|
252
263
|
profile?: undefined;
|
|
253
264
|
}>;
|
|
254
|
-
/**
|
|
255
|
-
* 页内拾取器(evaluate 注入):一次性 click 捕获 —— 点击任意元素时阻止默认行为,
|
|
256
|
-
* 计算稳定 CSS 路径 + 元素信息存 window.__dshPickResult(面板轮询读取后自清)。
|
|
257
|
-
* 与 aria walker 的 cssPath 同一套语义(#id → [data-testid] → nth-of-type 链)。
|
|
258
|
-
*/
|
|
259
|
-
export declare function pickElementInjector(): void;
|
|
260
|
-
/** 读取并清空拾取结果(ui.ts /pick GET 调用;无结果返回 null)。 */
|
|
261
|
-
export declare function takePickResult(page: Page): Promise<Record<string, unknown> | null>;
|
|
262
265
|
/**
|
|
263
266
|
* 操作预告高亮:agent 即将 click/type/select/press 的元素在页面上琥珀色描边
|
|
264
267
|
* ~1.5 秒。异步不阻塞(fire-and-forget,任何失败静默),不影响操作时序。
|
|
@@ -574,6 +577,7 @@ export declare function recordTool(args: Record<string, any>): Promise<{
|
|
|
574
577
|
hint: string;
|
|
575
578
|
warnings?: string[] | undefined;
|
|
576
579
|
varsApplied?: number | undefined;
|
|
580
|
+
skippedFailed?: number | undefined;
|
|
577
581
|
stripped?: number | undefined;
|
|
578
582
|
saved: string;
|
|
579
583
|
steps: number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-view",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DSH 插件:共享 Playwright 浏览器自动化(36 个 browser_* 工具,含语义元素查找、批处理、视觉回归、选择器自愈、参数化回放、结构化抽取、回归报告与隔离 context)+ 人机协作(
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "DSH 插件:共享 Playwright 浏览器自动化(36 个 browser_* 工具,含语义元素查找、批处理、视觉回归、选择器自愈、参数化回放、结构化抽取、回归报告与隔离 context)+ 人机协作(人工接管等待、敏感操作审批、人机混合录制、面板双向化:操作预告高亮/内嵌报告)+ 实时画面面板与录制管理。仅依赖 playwright-core,不依赖任何 @deepseek-ai 运行时包。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/types/index.d.ts",
|