dsh-browser-view 0.8.1 → 0.8.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 +17 -8
- package/lib/client.js +1 -1
- package/lib/index.js +95 -107
- package/lib/mcp.js +82 -109
- package/lib/types/browser/session/collaboration.d.ts +13 -0
- package/lib/types/browser/tools/collaboration.d.ts +16 -13
- package/lib/types/client/api.d.ts +4 -1
- package/lib/types/client/components/LoginWaitCard.d.ts +30 -0
- package/lib/types/client/hooks/useLoginWaitPolling.d.ts +6 -0
- package/lib/types/client/types.d.ts +7 -0
- package/lib/types/lib/prompts.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -225,9 +225,11 @@ config:
|
|
|
225
225
|
> `headless` 默认已是 `false`(有头窗口);服务器等无 GUI 场景请显式覆盖为 `true`。
|
|
226
226
|
|
|
227
227
|
1. agent 用 `browser_open` 打开登录页(此时弹出真实浏览器窗口);
|
|
228
|
-
2. agent 告知用户完成登录,并调用 `browser_wait_for_login`(可选 `successSelector` / `successUrl`
|
|
228
|
+
2. agent 告知用户完成登录,并调用 `browser_wait_for_login`(可选 `successSelector` / `successUrl` 指定确定性完成条件);
|
|
229
229
|
3. 用户在窗口中输入账号 / 扫码 / 完成双因素;
|
|
230
|
-
4.
|
|
230
|
+
4. 用户点 DSH 面板登录等待卡上的 **「我已完成登录」**(常驻悬浮,面板收起也可见),工具立即返回,agent 继续后续操作。
|
|
231
|
+
|
|
232
|
+
> 完成与否由用户点按钮说了算,不做任何登录状态自动猜测(启发式判定对登录表单隐藏不卸载/URL 仍含 auth 特征的 SPA 会永久漏判)。给了 `successSelector` / `successUrl` 时也支持自动判定;独立 MCP 模式没有面板,必须显式给二者之一,否则调用直接报错。
|
|
231
233
|
|
|
232
234
|
配置 `userDataDir` 后,登录态(Cookie / localStorage)会保存到该目录,后续即使切回 `headless: true` 或重启 DSH,已登录的网站仍保持登录态。
|
|
233
235
|
|
|
@@ -289,23 +291,30 @@ browser_confirm({
|
|
|
289
291
|
v0.8.0 起本包附带独立 MCP 入口 `dsh-browser-mcp`(stdio),DSH 之外的 MCP 客户端(Claude Desktop / ZCode / Cursor 等)可直接调用全部 39 个工具——走与 DSH 宿主**同一条** registerTools 装配链,审批闸/录制打点/时光机行为一致:
|
|
290
292
|
|
|
291
293
|
```jsonc
|
|
292
|
-
// 客户端 mcpServers
|
|
294
|
+
// 客户端 mcpServers 配置(推荐:先全局安装一次,再用绝对路径启动)
|
|
295
|
+
// 第一步:npm i -g dsh-browser-view
|
|
296
|
+
// 第二步:command 填 bin 绝对路径(`which dsh-browser-mcp` 查,如 /usr/local/bin/dsh-browser-mcp)
|
|
293
297
|
{
|
|
294
298
|
"mcpServers": {
|
|
295
299
|
"dsh-browser": {
|
|
296
|
-
"command": "
|
|
297
|
-
"args": ["-y", "dsh-browser-view"],
|
|
300
|
+
"command": "/usr/local/bin/dsh-browser-mcp",
|
|
298
301
|
"env": {
|
|
299
302
|
"DSH_BROWSER_HEADLESS": "true",
|
|
300
|
-
"DSH_BROWSER_USER_DATA_DIR": "
|
|
303
|
+
"DSH_BROWSER_USER_DATA_DIR": "/home/you/.dsh-browser/mcp-profile",
|
|
301
304
|
"DSH_BROWSER_TOOLSET": "core"
|
|
302
|
-
}
|
|
305
|
+
},
|
|
306
|
+
"timeoutMs": 60000
|
|
303
307
|
}
|
|
304
308
|
}
|
|
305
309
|
}
|
|
306
310
|
```
|
|
307
311
|
|
|
308
|
-
|
|
312
|
+
快速体验也可用 `"command": "npx", "args": ["-y", "dsh-browser-view"]`,但注意:
|
|
313
|
+
|
|
314
|
+
- **npx 冷安装可能超过客户端的 MCP 默认超时**(常见 30s):本包依赖 `playwright-core`,慢网络下首次下载数分钟,期间无响应会被判启动失败——务必加 `"timeoutMs": 120000`,或改用上面的全局安装方案(一次性下载不受超时约束,日常启动 1~2s);
|
|
315
|
+
- **env 里的路径必须写绝对路径**:`~` 不会被展开(环境变量注入不经过 shell),写 `~/.dsh-browser/...` 会在进程工作目录下建名为 `~` 的目录。
|
|
316
|
+
|
|
317
|
+
- 全局安装后两个 bin 别名等价(`dsh-browser-mcp` / `dsh-browser-view`);GUI 客户端(Claude Desktop 等)建议写绝对路径,避免 PATH 差异;
|
|
309
318
|
- 配置走环境变量 `DSH_BROWSER_<大写蛇形>` → 同名配置键(`TIMEOUT_MS`→`timeoutMs`),`true/false` 转 boolean、纯数字转 number、列表键逗号分隔;校验与默认值复用同一套(见上方配置表,不新增配置键);
|
|
310
319
|
- 工具使用指引由 server `instructions` 下发(与 DSH systemPrompt 同源),`what:"登录"` 语义定位等用法完全一致;
|
|
311
320
|
- 工具元数据支持 MCP 2025 标准 `annotations`(`readOnlyHint` / `destructiveHint` 等属性);
|
package/lib/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=null;async function a(){if(i!==null)return i;try{let e=await fetch(`/__dsh-browser-view__/config`,{cache:`no-store`});if(e.ok){let t=await e.json();if(typeof t?.basePath==`string`&&t.basePath)return i=t.basePath,t.basePath}}catch{}return i=`/browser`,i}async function o(e){try{let t=await fetch(`${e}/status`,{cache:`no-store`});return t.ok?await t.json():null}catch{return null}}async function s(e){try{let t=await fetch(`${e}/history`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.history)?n.history:[]}catch{return[]}}async function c(e){try{let t=await fetch(`${e}/log`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.calls)?n.calls:[]}catch{return[]}}async function l(e){try{let t=await fetch(`${e}/runs/latest`,{cache:`no-store`});if(!t.ok)return{run:null,busy:!1};let n=await t.json();return{run:n?.run??null,busy:n?.busy===!0}}catch{return{run:null,busy:!1}}}async function u(e){try{let t=await fetch(`${e}/confirm`,{cache:`no-store`});if(!t.ok)return null;let n=await t.json();return n?.confirm??n?.pending??null}catch{return null}}async function d(e,t,n){try{return(await fetch(`${e}/confirm`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,decision:n})})).ok}catch{return!1}}async function f(e){try{let t=await fetch(`${e}/pick`,{cache:`no-store`});return t.ok?(await t.json())?.pick??null:null}catch{return null}}async function p(e,t,n,r,i,a){try{return(await fetch(`${e}/pick`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,x:n,y:r,imgW:i,imgH:a})})).ok}catch{return!1}}async function m(e,t,n){try{let r=await fetch(`${e}/runs/rerun`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...n?{retryOf:n}:{}})});if(!r.ok){let e=await r.text().catch(()=>``);return{ok:!1,status:r.status,error:e}}return{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}async function h(e){try{let t=await fetch(`${e}/recordings`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.recordings)?n.recordings:[]}catch{return[]}}async function g(e,t){try{let n=await fetch(`${e}/recordings/detail?name=${encodeURIComponent(t)}`,{cache:`no-store`});return n.ok?await n.json():null}catch{return null}}async function _(e,t){try{return(await fetch(`${e}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:t})})).ok}catch{return!1}}function v(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)([]),[s,u]=(0,r.useState)(Date.now()),[d,f]=(0,r.useState)(null),[p,h]=(0,r.useState)(!1),[g,_]=(0,r.useState)(``);return(0,r.useEffect)(()=>{let t=!0,r=async()=>{let[r,i,s]=await Promise.all([o(e),c(e),l(e)]);t&&(n(r),r?.open&&u(Date.now()),a(i.slice(0,8)),f(s.run),h(s.busy))};r();let i=setInterval(r,3e3);return()=>{t=!1,clearInterval(i)}},[e]),{status:t,ops:i,shotVersion:s,lastRun:d,agentBusy:p,rerunMsg:g,rerunLast:async()=>{let t=d;if(!t||p)return;let n=t.source.replace(/\(已从磁盘重载\)$/,``);if(!n||[`steps`,`recording`,`recent-log`].includes(n)){_(`该来源不是已保存录制,无法一键重跑`);return}_(`重跑中…`),h(!0);let r=await m(e,n,t.runId);r.ok?_(`重跑完成,报告稍后刷新`):(h(!1),_(r.error??`重跑失败`))}}}function y(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await u(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingConfirm:t,resolveConfirm:async(t,r)=>{await d(e,t,r)&&n(null)}}}function b(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await f(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingPick:t}}let x={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},S={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function C(){let[e,t]=(0,r.useState)(()=>typeof document<`u`&&document.body?.getAttribute(`data-ds-dark-theme`)!==null);return(0,r.useEffect)(()=>{if(typeof document>`u`)return;let e=new MutationObserver(()=>t(document.body?.getAttribute(`data-ds-dark-theme`)!==null));return e.observe(document.body,{attributes:!0,attributeFilter:[`data-ds-dark-theme`]}),()=>e.disconnect()},[]),e}function w(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function T({confirm:e,dark:t,onDecision:n}){return(0,r.createElement)(`div`,{style:{padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #ef4444`,background:t?`rgba(239, 68, 68, 0.12)`:`rgba(239, 68, 68, 0.07)`,color:t?`#fca5a5`:`#b91c1c`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 请求执行敏感操作,请审批`},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`⚠`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`,fontWeight:600}},`敏感操作待审批`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`}},e.reason,e.tool?` (${e.tool})`:``),(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,marginTop:`6px`}},(0,r.createElement)(`button`,{onClick:()=>n(`allowed`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`允许`),(0,r.createElement)(`button`,{onClick:()=>n(`denied`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#dc2626`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`拒绝`)))}function E({streamSrc:e,shotSrc:t,alt:n,title:i,style:a,onClick:o}){let[s,c]=(0,r.useState)(!0),[l,u]=(0,r.useState)(0);(0,r.useEffect)(()=>{s||(u(e=>e+1),c(!0))},[t,s]);let d=s&&e!==void 0?l>0?`${e}${e.includes(`?`)?`&`:`?`}probe=${l}`:e:t;return(0,r.createElement)(`img`,{src:d,onError:()=>c(!1),alt:n,title:i,onClick:o,style:a})}function D({status:e,ops:t,lastRun:n,agentBusy:i,rerunMsg:a,pendingConfirm:o,pendingPick:s,shotSrc:c,streamSrc:l,dark:u,palette:d,onOpenModal:f,onOpenReport:p,onConfirmDecision:m,onRerunLast:h}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`320px`,maxHeight:`78vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${d.border}`,borderRadius:`12px`,background:u?`#181e29`:`#ffffff`,boxShadow:`0 8px 32px rgba(0, 0, 0, 0.3)`,padding:`12px`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,padding:`4px 0`}},(0,r.createElement)(`span`,{style:{width:`8px`,height:`8px`,borderRadius:`999px`,flex:`none`,background:e?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:d.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:d.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},e===null?`未启用`:e.open?`${e.tabs??1} 标签 · ${(e.url||``).slice(0,20)}`:`未打开`)),o?(0,r.createElement)(T,{confirm:o,dark:u,onDecision:m}):null,e?.pause?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 正在等待人工操作,请在浏览器窗口中完成`},(0,r.createElement)(`span`,{style:{flex:`none`}},`⏸`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待人工操作: ${e.pause.instruction}`)):null,s?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`,cursor:`pointer`},title:`点击放大实时画面,在画面上点选目标元素`,onClick:f},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${s.instruction}(点此放大画面)`)):null,e?.open?(0,r.createElement)(E,{streamSrc:l,shotSrc:c,alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:f,style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${d.border}`,cursor:`pointer`,background:u?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:u?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${t.length} 条)`),t.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{flex:`none`,color:e.ok?`#10b981`:`#ef4444`,fontWeight:600}},e.ok?`✓`:`✗`),(0,r.createElement)(`span`,{style:{color:d.tertiary,fontSize:`11px`,flex:`none`}},w(e.ts)),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`},title:`${e.tool}${e.error?`: ${e.error}`:``}`},`${e.tool}${e.error?`: ${e.error}`:``}`)))),n!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,cursor:`pointer`},title:`runId=${n.runId} · 点击查看报告`,onClick:p},(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:n.failed===0?`#10b981`:`#ef4444`}},n.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${n.source}${n.row===void 0?``:` 行${n.row}`} ${n.passed}/${n.total}${n.healedCount>0?` · 自愈${n.healedCount}`:``}${n.retries!==void 0&&n.retries.recovered>0?` · 重试挽回${n.retries.recovered}`:``} · ${new Date(n.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`),a!==``&&(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},a),(0,r.createElement)(`button`,{type:`button`,title:i?`agent 忙碌,稍后可重跑`:`重跑 ${n.source}(结果进运行历史,标 retryOf)`,disabled:i,onClick:e=>{e.stopPropagation(),h()},style:{flex:`none`,height:`20px`,padding:`0 8px`,border:`1px solid ${d.border}`,borderRadius:`6px`,background:`transparent`,color:i?d.tertiary:d.secondary,fontSize:`11px`,cursor:i?`not-allowed`:`pointer`,opacity:i?.5:1}},i?`重跑(忙碌)`:`↻ 重跑`)))}function O({status:e,history:t,shotSrc:n,streamSrc:i,dark:a,palette:o,onClose:s,pendingPick:c,onPick:l}){(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&s()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[s]);let u=e=>{if(c==null||l===void 0)return;let t=e.currentTarget,n=t.getBoundingClientRect(),r=t.naturalWidth||n.width,i=t.naturalHeight||n.height;if(r<=0||i<=0)return;let a=Math.min(n.width/r,n.height/i),o=(n.width-r*a)/2,s=(n.height-i*a)/2,u=Math.min(Math.max((e.clientX-n.left-o)/a,0),r),d=Math.min(Math.max((e.clientY-n.top-s)/a,0),i);l(Math.round(u),Math.round(d),Math.round(r),Math.round(i))},d=c!=null;return(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&s()},style:{position:`fixed`,inset:0,zIndex:2147483001,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1100px, 94vw)`,maxHeight:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${o.border}`,borderRadius:`12px`,background:a?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,color:o.primary,fontSize:`14px`,fontWeight:600}},e?.open?`${e.title||``} (${e.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:s,style:{width:`28px`,height:`28px`,border:`1px solid ${o.border}`,borderRadius:`6px`,background:`transparent`,color:o.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),d?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 10px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:a?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:a?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${c.instruction}(在下方画面上点击目标元素)`)):null,e?.open?(0,r.createElement)(E,{streamSrc:i,shotSrc:n,alt:`浏览器大图`,...d?{onClick:u}:{},style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:d?`2px dashed #f59e0b`:`1px solid ${o.border}`,background:a?`#10141c`:`#f8fafc`,cursor:d?`crosshair`:`default`}}):(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${o.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${t.length} 条,点击在新标签打开)`),t.map((e,t)=>(0,r.createElement)(`a`,{key:t,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:o.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:o.tertiary,flex:`none`}},w(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function k({base:e,dark:t,palette:n,onClose:i}){return(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&i()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[i]),(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&i()},style:{position:`fixed`,inset:0,zIndex:2147483002,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1000px, 94vw)`,height:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${n.border}`,borderRadius:`12px`,background:t?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:n.primary,fontSize:`14px`,fontWeight:600}},`回归测试报告 (最新运行)`),(0,r.createElement)(`a`,{href:`${e}/runs/report.html`,target:`_blank`,rel:`noopener noreferrer`,style:{color:n.secondary,fontSize:`12px`,textDecoration:`none`,padding:`4px 8px`,border:`1px solid ${n.border}`,borderRadius:`6px`}},`新标签页打开`),(0,r.createElement)(`button`,{type:`button`,onClick:i,style:{width:`28px`,height:`28px`,border:`1px solid ${n.border}`,borderRadius:`6px`,background:`transparent`,color:n.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),(0,r.createElement)(`iframe`,{src:`${e}/runs/report.html`,title:`回归测试报告`,style:{flex:1,width:`100%`,border:`1px solid ${n.border}`,borderRadius:`8px`,background:`#ffffff`}})))}function A(){let e=C(),t=e?S:x,[n,i]=(0,r.useState)(`/browser`),[o,c]=(0,r.useState)(!1),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[m,h]=(0,r.useState)([]);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&i(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{status:g,ops:_,shotVersion:w,lastRun:T,agentBusy:E,rerunMsg:A,rerunLast:j}=v(n),{pendingConfirm:M,resolveConfirm:N}=y(n),{pendingPick:P}=b(n);(0,r.useEffect)(()=>{if(!l)return;let e=!0,t=async()=>{let t=await s(n);e&&h(t)};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[l,n]);let F=`${n}/screenshot?v=${w}`,I=`${n}/stream`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>c(e=>!e),style:{position:`fixed`,right:`16px`,bottom:`16px`,width:`42px`,height:`42px`,borderRadius:`999px`,border:`1px solid ${t.border}`,background:e?`#1e2430`:`#ffffff`,color:t.primary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`18px`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.2)`,transition:`transform 0.15s ease, background 0.15s ease`,pointerEvents:`auto`}},`🌐`),o&&(0,r.createElement)(D,{status:g,ops:_,lastRun:T,agentBusy:E,rerunMsg:A,pendingConfirm:M,pendingPick:P,shotSrc:F,streamSrc:I,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{M&&N(M.id,e)},onRerunLast:j}),l&&(0,r.createElement)(O,{status:g,history:m,shotSrc:F,streamSrc:I,dark:e,palette:t,onClose:()=>u(!1),pendingPick:P,onPick:(e,t,r,i)=>{P&&p(n,P.id,e,t,r,i)}}),d&&(0,r.createElement)(k,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function j(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)(``),[o,s]=(0,r.useState)({});return(0,r.useEffect)(()=>{let t=!0;return n(null),a(``),h(e).then(e=>{t&&n(e)}).catch(e=>{t&&a(e instanceof Error?e.message:String(e))}),()=>{t=!1}},[e]),{list:t,error:i,expanded:o,toggleDetail:t=>{s(n=>{let r=n[t];if(r!==void 0&&r!==`loading`){let e={...n};return delete e[t],e}return g(e,t).then(e=>{s(n=>({...n,[t]:e??`failed`}))}).catch(()=>s(e=>({...e,[t]:`failed`}))),{...n,[t]:`loading`}})},remove:async t=>{await _(e,t)&&(n(e=>e===null?e:e.filter(e=>e.name!==t)),s(e=>{let n={...e};return delete n[t],n}))}}}function M(e){return{row:{display:`flex`,alignItems:`center`,gap:`8px`,minWidth:0},label:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,fontSize:`13px`,color:e.primary,cursor:`pointer`},meta:{color:e.tertiary,fontSize:`12px`,whiteSpace:`nowrap`},btn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.secondary,font:`inherit`,fontSize:`12px`,cursor:`pointer`},delBtn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.danger,font:`inherit`,fontSize:`12px`,cursor:`pointer`},itemBox:{display:`flex`,flexDirection:`column`,gap:`6px`,border:`1px solid ${e.border}`,borderRadius:`8px`,padding:`8px 10px`},stepsBox:{display:`flex`,flexDirection:`column`,gap:`2px`,borderTop:`1px solid ${e.border}`,paddingTop:`6px`},stepLine:{display:`flex`,gap:`8px`,fontSize:`13px`,lineHeight:`1.7`,color:e.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`},toolName:{color:e.primary,flex:`none`,fontWeight:500},stepArgs:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,color:e.secondary},empty:{color:e.tertiary,fontSize:`12px`,textAlign:`center`,padding:`12px 0`},root:{display:`flex`,flexDirection:`column`,gap:`8px`,maxWidth:`720px`},heading:{color:e.tertiary,fontSize:`12px`,lineHeight:`1.6`}}}function N(e){let t=C(),n=t?S:x,i=M(n),[o,s]=(0,r.useState)(`/browser`);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&s(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{list:c,error:l,expanded:u,toggleDetail:d,remove:f}=j(o);return(0,r.createElement)(`div`,{style:i.root},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,paddingBottom:`12px`,marginBottom:`6px`,borderBottom:`1px solid ${n.border}`}},(0,r.createElement)(`div`,{style:{width:`36px`,height:`36px`,borderRadius:`8px`,background:t?`rgba(99, 102, 241, 0.15)`:`rgba(99, 102, 241, 0.1)`,border:`1px solid ${t?`rgba(99, 102, 241, 0.3)`:`rgba(99, 102, 241, 0.2)`}`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`20px`,flex:`none`}},`🌐`),(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`2px`}},(0,r.createElement)(`div`,{style:{fontSize:`15px`,fontWeight:600,color:n.primary}},`浏览器自动化与录制管理`),(0,r.createElement)(`div`,{style:{fontSize:`12px`,color:n.tertiary}},`管理已保存的浏览器回归脚本,查看执行序列与历史数据`))),(0,r.createElement)(`div`,{style:i.heading},`已保存的录制(由 browser_record save 生成,供 browser_replay 一键回放):`),l?(0,r.createElement)(`div`,{style:{...i.empty,color:n.danger}},`加载失败: ${l}`):c===null?(0,r.createElement)(`div`,{style:i.empty},`加载中…`):c.length===0?(0,r.createElement)(`div`,{style:i.empty},`暂无已保存录制。在与 Agent 对话中完成一轮浏览器操作后,输入「保存录制为 xxx」即可沉淀为回归用例。`):c.map(e=>{let t=u[e.name],n=t!==void 0&&t!==`loading`;return(0,r.createElement)(`div`,{key:e.name,style:i.itemBox},(0,r.createElement)(`div`,{style:i.row},(0,r.createElement)(`span`,{style:i.label,title:`点击展开 / 折叠步骤详情`,onClick:()=>d(e.name)},(n?`▾ `:`▸ `)+e.name),(0,r.createElement)(`span`,{style:i.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleDateString(`zh-CN`)}`),(0,r.createElement)(`button`,{type:`button`,style:i.btn,onClick:()=>d(e.name)},n?`收起`:`步骤`),(0,r.createElement)(`button`,{type:`button`,style:i.delBtn,onClick:()=>f(e.name)},`删除`)),n&&t!==`failed`&&(0,r.createElement)(`div`,{style:i.stepsBox},t.steps.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:i.stepLine},(0,r.createElement)(`span`,{style:i.meta},`#${t+1}`),(0,r.createElement)(`span`,{style:i.toolName},e.tool),(0,r.createElement)(`span`,{style:i.stepArgs},JSON.stringify(e.args))))))}))}let P=[`slots`];function F(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},N)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},A))}return n.apply=F,n.inject=P,t.exports}});
|
|
1
|
+
window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=null;async function a(){if(i!==null)return i;try{let e=await fetch(`/__dsh-browser-view__/config`,{cache:`no-store`});if(e.ok){let t=await e.json();if(typeof t?.basePath==`string`&&t.basePath)return i=t.basePath,t.basePath}}catch{}return i=`/browser`,i}async function o(e){try{let t=await fetch(`${e}/status`,{cache:`no-store`});return t.ok?await t.json():null}catch{return null}}async function s(e){try{let t=await fetch(`${e}/history`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.history)?n.history:[]}catch{return[]}}async function c(e){try{let t=await fetch(`${e}/log`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.calls)?n.calls:[]}catch{return[]}}async function l(e){try{let t=await fetch(`${e}/runs/latest`,{cache:`no-store`});if(!t.ok)return{run:null,busy:!1};let n=await t.json();return{run:n?.run??null,busy:n?.busy===!0}}catch{return{run:null,busy:!1}}}async function u(e){try{let t=await fetch(`${e}/confirm`,{cache:`no-store`});if(!t.ok)return null;let n=await t.json();return n?.confirm??n?.pending??null}catch{return null}}async function d(e,t,n){try{return(await fetch(`${e}/confirm`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,decision:n})})).ok}catch{return!1}}async function f(e){try{let t=await fetch(`${e}/pick`,{cache:`no-store`});return t.ok?(await t.json())?.pick??null:null}catch{return null}}async function p(e){try{let t=await fetch(`${e}/login`,{cache:`no-store`});return t.ok?(await t.json())?.loginWait??null:null}catch{return null}}async function m(e,t){try{return(await fetch(`${e}/login`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t})})).ok}catch{return!1}}async function h(e,t,n,r,i,a){try{return(await fetch(`${e}/pick`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t,x:n,y:r,imgW:i,imgH:a})})).ok}catch{return!1}}async function g(e,t,n){try{let r=await fetch(`${e}/runs/rerun`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...n?{retryOf:n}:{}})});if(!r.ok){let e=await r.text().catch(()=>``);return{ok:!1,status:r.status,error:e}}return{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}async function _(e){try{let t=await fetch(`${e}/recordings`,{cache:`no-store`});if(!t.ok)return[];let n=await t.json();return Array.isArray(n?.recordings)?n.recordings:[]}catch{return[]}}async function v(e,t){try{let n=await fetch(`${e}/recordings/detail?name=${encodeURIComponent(t)}`,{cache:`no-store`});return n.ok?await n.json():null}catch{return null}}async function y(e,t){try{return(await fetch(`${e}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:t})})).ok}catch{return!1}}function b(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)([]),[s,u]=(0,r.useState)(Date.now()),[d,f]=(0,r.useState)(null),[p,m]=(0,r.useState)(!1),[h,_]=(0,r.useState)(``);return(0,r.useEffect)(()=>{let t=!0,r=async()=>{let[r,i,s]=await Promise.all([o(e),c(e),l(e)]);t&&(n(r),r?.open&&u(Date.now()),a(i.slice(0,8)),f(s.run),m(s.busy))};r();let i=setInterval(r,3e3);return()=>{t=!1,clearInterval(i)}},[e]),{status:t,ops:i,shotVersion:s,lastRun:d,agentBusy:p,rerunMsg:h,rerunLast:async()=>{let t=d;if(!t||p)return;let n=t.source.replace(/\(已从磁盘重载\)$/,``);if(!n||[`steps`,`recording`,`recent-log`].includes(n)){_(`该来源不是已保存录制,无法一键重跑`);return}_(`重跑中…`),m(!0);let r=await g(e,n,t.runId);r.ok?_(`重跑完成,报告稍后刷新`):(m(!1),_(r.error??`重跑失败`))}}}function x(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await u(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingConfirm:t,resolveConfirm:async(t,r)=>{await d(e,t,r)&&n(null)}}}function S(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await p(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{loginWait:t,confirmLoginDone:async t=>{await m(e,t)&&n(null)}}}function C(e){let[t,n]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let t=!0,r=0,i,a=async()=>{try{let i=await f(e);if(!t)return;r=0,n(i)}catch{r++,t&&n(null)}finally{if(!t)return;i=setTimeout(a,r>=3?15e3:1e3)}};return a(),()=>{t=!1,i!==void 0&&clearTimeout(i)}},[e]),{pendingPick:t}}let w={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},T={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function E(){let[e,t]=(0,r.useState)(()=>typeof document<`u`&&document.body?.getAttribute(`data-ds-dark-theme`)!==null);return(0,r.useEffect)(()=>{if(typeof document>`u`)return;let e=new MutationObserver(()=>t(document.body?.getAttribute(`data-ds-dark-theme`)!==null));return e.observe(document.body,{attributes:!0,attributeFilter:[`data-ds-dark-theme`]}),()=>e.disconnect()},[]),e}function D(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function O({confirm:e,dark:t,onDecision:n}){return(0,r.createElement)(`div`,{style:{padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #ef4444`,background:t?`rgba(239, 68, 68, 0.12)`:`rgba(239, 68, 68, 0.07)`,color:t?`#fca5a5`:`#b91c1c`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 请求执行敏感操作,请审批`},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`⚠`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`,fontWeight:600}},`敏感操作待审批`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`}},e.reason,e.tool?` (${e.tool})`:``),(0,r.createElement)(`div`,{style:{display:`flex`,gap:`8px`,marginTop:`6px`}},(0,r.createElement)(`button`,{onClick:()=>n(`allowed`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`允许`),(0,r.createElement)(`button`,{onClick:()=>n(`denied`),style:{flex:1,padding:`4px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#dc2626`,color:`#fff`,fontSize:`12px`,fontWeight:600}},`拒绝`)))}function k({streamSrc:e,shotSrc:t,alt:n,title:i,style:a,onClick:o}){let[s,c]=(0,r.useState)(!0),[l,u]=(0,r.useState)(0);(0,r.useEffect)(()=>{s||(u(e=>e+1),c(!0))},[t,s]);let d=s&&e!==void 0?l>0?`${e}${e.includes(`?`)?`&`:`?`}probe=${l}`:e:t;return(0,r.createElement)(`img`,{src:d,onError:()=>c(!1),alt:n,title:i,onClick:o,style:a})}function A({status:e,ops:t,lastRun:n,agentBusy:i,rerunMsg:a,pendingConfirm:o,pendingPick:s,shotSrc:c,streamSrc:l,dark:u,palette:d,onOpenModal:f,onOpenReport:p,onConfirmDecision:m,onRerunLast:h}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`320px`,maxHeight:`78vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${d.border}`,borderRadius:`12px`,background:u?`#181e29`:`#ffffff`,boxShadow:`0 8px 32px rgba(0, 0, 0, 0.3)`,padding:`12px`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,padding:`4px 0`}},(0,r.createElement)(`span`,{style:{width:`8px`,height:`8px`,borderRadius:`999px`,flex:`none`,background:e?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:d.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:d.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},e===null?`未启用`:e.open?`${e.tabs??1} 标签 · ${(e.url||``).slice(0,20)}`:`未打开`)),o?(0,r.createElement)(O,{confirm:o,dark:u,onDecision:m}):null,e?.pause?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px solid #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`},title:`Agent 正在等待人工操作,请在浏览器窗口中完成`},(0,r.createElement)(`span`,{style:{flex:`none`}},`⏸`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待人工操作: ${e.pause.instruction}`)):null,s?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 8px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:u?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:u?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`,cursor:`pointer`},title:`点击放大实时画面,在画面上点选目标元素`,onClick:f},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${s.instruction}(点此放大画面)`)):null,e?.open?(0,r.createElement)(k,{streamSrc:l,shotSrc:c,alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:f,style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${d.border}`,cursor:`pointer`,background:u?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:u?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:d.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${t.length} 条)`),t.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{flex:`none`,color:e.ok?`#10b981`:`#ef4444`,fontWeight:600}},e.ok?`✓`:`✗`),(0,r.createElement)(`span`,{style:{color:d.tertiary,fontSize:`11px`,flex:`none`}},D(e.ts)),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`},title:`${e.tool}${e.error?`: ${e.error}`:``}`},`${e.tool}${e.error?`: ${e.error}`:``}`)))),n!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${d.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:d.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,cursor:`pointer`},title:`runId=${n.runId} · 点击查看报告`,onClick:p},(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:n.failed===0?`#10b981`:`#ef4444`}},n.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${n.source}${n.row===void 0?``:` 行${n.row}`} ${n.passed}/${n.total}${n.healedCount>0?` · 自愈${n.healedCount}`:``}${n.retries!==void 0&&n.retries.recovered>0?` · 重试挽回${n.retries.recovered}`:``} · ${new Date(n.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`),a!==``&&(0,r.createElement)(`span`,{style:{flex:`none`,color:d.tertiary,fontSize:`11px`}},a),(0,r.createElement)(`button`,{type:`button`,title:i?`agent 忙碌,稍后可重跑`:`重跑 ${n.source}(结果进运行历史,标 retryOf)`,disabled:i,onClick:e=>{e.stopPropagation(),h()},style:{flex:`none`,height:`20px`,padding:`0 8px`,border:`1px solid ${d.border}`,borderRadius:`6px`,background:`transparent`,color:i?d.tertiary:d.secondary,fontSize:`11px`,cursor:i?`not-allowed`:`pointer`,opacity:i?.5:1}},i?`重跑(忙碌)`:`↻ 重跑`)))}function j({status:e,history:t,shotSrc:n,streamSrc:i,dark:a,palette:o,onClose:s,pendingPick:c,onPick:l}){(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&s()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[s]);let u=e=>{if(c==null||l===void 0)return;let t=e.currentTarget,n=t.getBoundingClientRect(),r=t.naturalWidth||n.width,i=t.naturalHeight||n.height;if(r<=0||i<=0)return;let a=Math.min(n.width/r,n.height/i),o=(n.width-r*a)/2,s=(n.height-i*a)/2,u=Math.min(Math.max((e.clientX-n.left-o)/a,0),r),d=Math.min(Math.max((e.clientY-n.top-s)/a,0),i);l(Math.round(u),Math.round(d),Math.round(r),Math.round(i))},d=c!=null;return(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&s()},style:{position:`fixed`,inset:0,zIndex:2147483001,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1100px, 94vw)`,maxHeight:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${o.border}`,borderRadius:`12px`,background:a?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,color:o.primary,fontSize:`14px`,fontWeight:600}},e?.open?`${e.title||``} (${e.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:s,style:{width:`28px`,height:`28px`,border:`1px solid ${o.border}`,borderRadius:`6px`,background:`transparent`,color:o.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),d?(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,padding:`6px 10px`,borderRadius:`8px`,border:`1px dashed #f59e0b`,background:a?`rgba(245, 158, 11, 0.12)`:`rgba(245, 158, 11, 0.08)`,color:a?`#fbbf24`:`#b45309`,fontSize:`12px`,lineHeight:`1.5`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`🎯`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,wordBreak:`break-all`}},`等待点选教学: ${c.instruction}(在下方画面上点击目标元素)`)):null,e?.open?(0,r.createElement)(k,{streamSrc:i,shotSrc:n,alt:`浏览器大图`,...d?{onClick:u}:{},style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:d?`2px dashed #f59e0b`:`1px solid ${o.border}`,background:a?`#10141c`:`#f8fafc`,cursor:d?`crosshair`:`default`}}):(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),t.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${o.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:o.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${t.length} 条,点击在新标签打开)`),t.map((e,t)=>(0,r.createElement)(`a`,{key:t,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:o.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:o.tertiary,flex:`none`}},D(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url))))))}function M({base:e,dark:t,palette:n,onClose:i}){return(0,r.useEffect)(()=>{let e=e=>{e.key===`Escape`&&i()};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[i]),(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&i()},style:{position:`fixed`,inset:0,zIndex:2147483002,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1000px, 94vw)`,height:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${n.border}`,borderRadius:`12px`,background:t?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:n.primary,fontSize:`14px`,fontWeight:600}},`回归测试报告 (最新运行)`),(0,r.createElement)(`a`,{href:`${e}/runs/report.html`,target:`_blank`,rel:`noopener noreferrer`,style:{color:n.secondary,fontSize:`12px`,textDecoration:`none`,padding:`4px 8px`,border:`1px solid ${n.border}`,borderRadius:`6px`}},`新标签页打开`),(0,r.createElement)(`button`,{type:`button`,onClick:i,style:{width:`28px`,height:`28px`,border:`1px solid ${n.border}`,borderRadius:`6px`,background:`transparent`,color:n.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),(0,r.createElement)(`iframe`,{src:`${e}/runs/report.html`,title:`回归测试报告`,style:{flex:1,width:`100%`,border:`1px solid ${n.border}`,borderRadius:`8px`,background:`#ffffff`}})))}function N({loginWait:e,onDone:t}){return(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`68px`,width:`260px`,padding:`10px 12px`,borderRadius:`10px`,border:`1px solid #16a34a`,background:`rgba(22, 163, 74, 0.12)`,color:`#15803d`,fontSize:`12px`,lineHeight:`1.5`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.18)`,pointerEvents:`auto`},title:`Agent 正在等待你完成登录:请在浏览器窗口登录后点击下方按钮`},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`}},(0,r.createElement)(`span`,{style:{flex:`none`}},`🔐`),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,fontWeight:600,wordBreak:`break-all`}},`等待人工登录`)),(0,r.createElement)(`div`,{style:{marginTop:`4px`,wordBreak:`break-all`,opacity:.9}},e.instruction),(0,r.createElement)(`button`,{onClick:t,style:{width:`100%`,marginTop:`8px`,padding:`6px 0`,borderRadius:`6px`,border:`none`,cursor:`pointer`,background:`#16a34a`,color:`#fff`,fontSize:`13px`,fontWeight:600}},`我已完成登录`))}function P(){let e=E(),t=e?T:w,[n,i]=(0,r.useState)(`/browser`),[o,c]=(0,r.useState)(!1),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[p,m]=(0,r.useState)([]);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&i(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{status:g,ops:_,shotVersion:v,lastRun:y,agentBusy:D,rerunMsg:O,rerunLast:k}=b(n),{pendingConfirm:P,resolveConfirm:F}=x(n),{loginWait:I,confirmLoginDone:L}=S(n),{pendingPick:R}=C(n);(0,r.useEffect)(()=>{if(!l)return;let e=!0,t=async()=>{let t=await s(n);e&&m(t)};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[l,n]);let z=`${n}/screenshot?v=${v}`,B=`${n}/stream`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},I&&!I.done?(0,r.createElement)(N,{loginWait:I,onDone:()=>{L(I.id)}}):null,(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>c(e=>!e),style:{position:`fixed`,right:`16px`,bottom:`16px`,width:`42px`,height:`42px`,borderRadius:`999px`,border:`1px solid ${t.border}`,background:e?`#1e2430`:`#ffffff`,color:t.primary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`18px`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.2)`,transition:`transform 0.15s ease, background 0.15s ease`,pointerEvents:`auto`}},`🌐`),o&&(0,r.createElement)(A,{status:g,ops:_,lastRun:y,agentBusy:D,rerunMsg:O,pendingConfirm:P,pendingPick:R,shotSrc:z,streamSrc:B,dark:e,palette:t,onOpenModal:()=>u(!0),onOpenReport:()=>f(!0),onConfirmDecision:e=>{P&&F(P.id,e)},onRerunLast:k}),l&&(0,r.createElement)(j,{status:g,history:p,shotSrc:z,streamSrc:B,dark:e,palette:t,onClose:()=>u(!1),pendingPick:R,onPick:(e,t,r,i)=>{R&&h(n,R.id,e,t,r,i)}}),d&&(0,r.createElement)(M,{base:n,dark:e,palette:t,onClose:()=>f(!1)}))}function F(e){let[t,n]=(0,r.useState)(null),[i,a]=(0,r.useState)(``),[o,s]=(0,r.useState)({});return(0,r.useEffect)(()=>{let t=!0;return n(null),a(``),_(e).then(e=>{t&&n(e)}).catch(e=>{t&&a(e instanceof Error?e.message:String(e))}),()=>{t=!1}},[e]),{list:t,error:i,expanded:o,toggleDetail:t=>{s(n=>{let r=n[t];if(r!==void 0&&r!==`loading`){let e={...n};return delete e[t],e}return v(e,t).then(e=>{s(n=>({...n,[t]:e??`failed`}))}).catch(()=>s(e=>({...e,[t]:`failed`}))),{...n,[t]:`loading`}})},remove:async t=>{await y(e,t)&&(n(e=>e===null?e:e.filter(e=>e.name!==t)),s(e=>{let n={...e};return delete n[t],n}))}}}function I(e){return{row:{display:`flex`,alignItems:`center`,gap:`8px`,minWidth:0},label:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,fontSize:`13px`,color:e.primary,cursor:`pointer`},meta:{color:e.tertiary,fontSize:`12px`,whiteSpace:`nowrap`},btn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.secondary,font:`inherit`,fontSize:`12px`,cursor:`pointer`},delBtn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.danger,font:`inherit`,fontSize:`12px`,cursor:`pointer`},itemBox:{display:`flex`,flexDirection:`column`,gap:`6px`,border:`1px solid ${e.border}`,borderRadius:`8px`,padding:`8px 10px`},stepsBox:{display:`flex`,flexDirection:`column`,gap:`2px`,borderTop:`1px solid ${e.border}`,paddingTop:`6px`},stepLine:{display:`flex`,gap:`8px`,fontSize:`13px`,lineHeight:`1.7`,color:e.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`},toolName:{color:e.primary,flex:`none`,fontWeight:500},stepArgs:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,color:e.secondary},empty:{color:e.tertiary,fontSize:`12px`,textAlign:`center`,padding:`12px 0`},root:{display:`flex`,flexDirection:`column`,gap:`8px`,maxWidth:`720px`},heading:{color:e.tertiary,fontSize:`12px`,lineHeight:`1.6`}}}function L(e){let t=E(),n=t?T:w,i=I(n),[o,s]=(0,r.useState)(`/browser`);(0,r.useEffect)(()=>{let e=!0;return a().then(t=>{e&&s(t.replace(/\/+$/,``))}),()=>{e=!1}},[]);let{list:c,error:l,expanded:u,toggleDetail:d,remove:f}=F(o);return(0,r.createElement)(`div`,{style:i.root},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,paddingBottom:`12px`,marginBottom:`6px`,borderBottom:`1px solid ${n.border}`}},(0,r.createElement)(`div`,{style:{width:`36px`,height:`36px`,borderRadius:`8px`,background:t?`rgba(99, 102, 241, 0.15)`:`rgba(99, 102, 241, 0.1)`,border:`1px solid ${t?`rgba(99, 102, 241, 0.3)`:`rgba(99, 102, 241, 0.2)`}`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`20px`,flex:`none`}},`🌐`),(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`2px`}},(0,r.createElement)(`div`,{style:{fontSize:`15px`,fontWeight:600,color:n.primary}},`浏览器自动化与录制管理`),(0,r.createElement)(`div`,{style:{fontSize:`12px`,color:n.tertiary}},`管理已保存的浏览器回归脚本,查看执行序列与历史数据`))),(0,r.createElement)(`div`,{style:i.heading},`已保存的录制(由 browser_record save 生成,供 browser_replay 一键回放):`),l?(0,r.createElement)(`div`,{style:{...i.empty,color:n.danger}},`加载失败: ${l}`):c===null?(0,r.createElement)(`div`,{style:i.empty},`加载中…`):c.length===0?(0,r.createElement)(`div`,{style:i.empty},`暂无已保存录制。在与 Agent 对话中完成一轮浏览器操作后,输入「保存录制为 xxx」即可沉淀为回归用例。`):c.map(e=>{let t=u[e.name],n=t!==void 0&&t!==`loading`;return(0,r.createElement)(`div`,{key:e.name,style:i.itemBox},(0,r.createElement)(`div`,{style:i.row},(0,r.createElement)(`span`,{style:i.label,title:`点击展开 / 折叠步骤详情`,onClick:()=>d(e.name)},(n?`▾ `:`▸ `)+e.name),(0,r.createElement)(`span`,{style:i.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleDateString(`zh-CN`)}`),(0,r.createElement)(`button`,{type:`button`,style:i.btn,onClick:()=>d(e.name)},n?`收起`:`步骤`),(0,r.createElement)(`button`,{type:`button`,style:i.delBtn,onClick:()=>f(e.name)},`删除`)),n&&t!==`failed`&&(0,r.createElement)(`div`,{style:i.stepsBox},t.steps.map((e,t)=>(0,r.createElement)(`div`,{key:t,style:i.stepLine},(0,r.createElement)(`span`,{style:i.meta},`#${t+1}`),(0,r.createElement)(`span`,{style:i.toolName},e.tool),(0,r.createElement)(`span`,{style:i.stepArgs},JSON.stringify(e.args))))))}))}let R=[`slots`];function z(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},L)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},P))}return n.apply=z,n.inject=R,t.exports}});
|
|
2
2
|
//# sourceMappingURL=client.js.map
|
package/lib/index.js
CHANGED
|
@@ -178,8 +178,8 @@ const BROWSER_DEFAULT_CONFIG = {
|
|
|
178
178
|
};
|
|
179
179
|
//#endregion
|
|
180
180
|
//#region lib/types/lib/prompts.js
|
|
181
|
-
const FULL_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话,支持多标签页与多配置文件。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),页内打分匹配并自动注册 ref,一次调用替代 快照→查找→点击 三轮;零命中报错会附最相近候选。需要精确控制时再用 selector(CSS)/ref(aria 快照)。打开页面后可先 browser_snapshot 读文本与链接、browser_elements 读表单结构(含关联 label 与展开的下拉选项);browser_find(query 传名称,只返回命中 ref+区域上下文,大页面最省 token;零命中会给相近候选)适合多目标排查,需要全页结构时再用 browser_snapshot { mode: \"aria\" }(可交互元素带 ref=eN,click/type/select/press 直接传 ref 代替 CSS 选择器);下拉选择(原生 select 与 antd/element 组件库)用 browser_select 传 selector/ref/what + option 文本一步完成;固定小序列(点击→等待→输入→提交)用 browser_batch 一次执行减少往返;标签管理用 browser_tabs;下载用 browser_download(wait=true 可等新下载);登录态用 browser_cookies。做自动化测试时:用 browser_wait_for 显式等待页面稳定/元素出现,用 browser_assert 断言(失败自动截图;visual={baseline} 可做视觉回归,先用 browser_screenshot { baseline } 建基线),用 browser_network 检查接口调用与 4xx/5xx,跑完一轮可用 browser_record save 保存操作序列、browser_replay 回放做回归(选择器失效自动按指纹自愈;vars/data 支持参数化与数据驱动)。每次 replay/batch 都记入运行历史,browser_report list/export 可查看与导出回归报告。抓取结构化数据用 browser_extract(fields 定义 CSS 字段,可翻页,可导出 CSV/JSONL)。遇到需要登录的网站:先 browser_open
|
|
182
|
-
const CORE_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话(toolset=core:已精简工具集)。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),一次调用替代快照→查找→点击;零命中报错附最相近候选。需要精确控制时再用 selector/ref:打开页面后用 browser_snapshot 读内容(scope 限定区域、since 增量重读省 token),browser_find(query 传名称,返回 ref),点击/输入/按键/下拉用 browser_click/browser_type/browser_press/browser_select(selector 或 ref);固定小序列用 browser_batch;等待用 browser_wait/browser_wait_for,断言用 browser_assert;接口检查用 browser_network;标签 browser_tabs;下载/上传 browser_download/browser_upload。需要人工:登录用 browser_wait_for_login,验证码/二次确认用 browser_pause_for_human,敏感动作(提交/删除/支付)前用 browser_confirm。环境异常先跑 browser_doctor 诊断。录制/回放/报告/抽取/profile/隔离 context 等工具当前未注册(toolset=core),需要时把配置改为 toolset: all 并重启 DSH。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
181
|
+
const FULL_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话,支持多标签页与多配置文件。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),页内打分匹配并自动注册 ref,一次调用替代 快照→查找→点击 三轮;零命中报错会附最相近候选。需要精确控制时再用 selector(CSS)/ref(aria 快照)。打开页面后可先 browser_snapshot 读文本与链接、browser_elements 读表单结构(含关联 label 与展开的下拉选项);browser_find(query 传名称,只返回命中 ref+区域上下文,大页面最省 token;零命中会给相近候选)适合多目标排查,需要全页结构时再用 browser_snapshot { mode: \"aria\" }(可交互元素带 ref=eN,click/type/select/press 直接传 ref 代替 CSS 选择器);下拉选择(原生 select 与 antd/element 组件库)用 browser_select 传 selector/ref/what + option 文本一步完成;固定小序列(点击→等待→输入→提交)用 browser_batch 一次执行减少往返;标签管理用 browser_tabs;下载用 browser_download(wait=true 可等新下载);登录态用 browser_cookies。做自动化测试时:用 browser_wait_for 显式等待页面稳定/元素出现,用 browser_assert 断言(失败自动截图;visual={baseline} 可做视觉回归,先用 browser_screenshot { baseline } 建基线),用 browser_network 检查接口调用与 4xx/5xx,跑完一轮可用 browser_record save 保存操作序列、browser_replay 回放做回归(选择器失效自动按指纹自愈;vars/data 支持参数化与数据驱动)。每次 replay/batch 都记入运行历史,browser_report list/export 可查看与导出回归报告。抓取结构化数据用 browser_extract(fields 定义 CSS 字段,可翻页,可导出 CSV/JSONL)。遇到需要登录的网站:先 browser_open 打开登录页,告知用户在浏览器窗口完成登录后点面板登录卡上的「我已完成登录」,再调用 browser_wait_for_login 等待(用户点按钮即返回,不自动猜测登录状态;也可给 successSelector/successUrl 作确定性自动条件;无面板环境必须给其一,否则调用会报错);验证码/二次确认等更广的「人补一步」场景用 browser_pause_for_human(提示语会显示在实时画面面板);配置 userDataDir 时登录态会持久化。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
182
|
+
const CORE_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话(toolset=core:已精简工具集)。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),一次调用替代快照→查找→点击;零命中报错附最相近候选。需要精确控制时再用 selector/ref:打开页面后用 browser_snapshot 读内容(scope 限定区域、since 增量重读省 token),browser_find(query 传名称,返回 ref),点击/输入/按键/下拉用 browser_click/browser_type/browser_press/browser_select(selector 或 ref);固定小序列用 browser_batch;等待用 browser_wait/browser_wait_for,断言用 browser_assert;接口检查用 browser_network;标签 browser_tabs;下载/上传 browser_download/browser_upload。需要人工:登录用 browser_wait_for_login(用户在面板点「我已完成登录」即返回;无面板环境必须给 successSelector/successUrl,否则调用报错),验证码/二次确认用 browser_pause_for_human,敏感动作(提交/删除/支付)前用 browser_confirm。环境异常先跑 browser_doctor 诊断。录制/回放/报告/抽取/profile/隔离 context 等工具当前未注册(toolset=core),需要时把配置改为 toolset: all 并重启 DSH。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
183
183
|
/** confirmMode 附加指引(与 DSH 入口同款语义,manual 面向宿主审批、auto 说明硬闸)。 */
|
|
184
184
|
function confirmGuidanceOf(confirmMode) {
|
|
185
185
|
if (confirmMode === "manual") return "本环境 confirmMode 为 manual:凡提交/删除/支付等不可逆或影响线上数据的敏感动作,执行前必须先调用 browser_confirm(reason 说明操作与影响,等待用户在实时画面面板点允许/拒绝;拒绝或超时即放弃该动作,不得换路径绕过)。";
|
|
@@ -600,6 +600,22 @@ function resolveConfirm(id, decision) {
|
|
|
600
600
|
pendingConfirm.decision = decision;
|
|
601
601
|
return true;
|
|
602
602
|
}
|
|
603
|
+
let pendingLoginWait = null;
|
|
604
|
+
function setPendingLoginWait(entry) {
|
|
605
|
+
pendingLoginWait = entry;
|
|
606
|
+
}
|
|
607
|
+
function getPendingLoginWait() {
|
|
608
|
+
return pendingLoginWait;
|
|
609
|
+
}
|
|
610
|
+
function clearPendingLoginWait() {
|
|
611
|
+
pendingLoginWait = null;
|
|
612
|
+
}
|
|
613
|
+
/** 面板提交「用户已完成登录」;id 不匹配(已被新请求取代/超时清理)时返回 false。 */
|
|
614
|
+
function resolveLoginWait(id) {
|
|
615
|
+
if (pendingLoginWait === null || pendingLoginWait.id !== id) return false;
|
|
616
|
+
pendingLoginWait.done = true;
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
603
619
|
let pendingPick = null;
|
|
604
620
|
function setPendingPick(entry) {
|
|
605
621
|
pendingPick = entry;
|
|
@@ -6223,57 +6239,49 @@ function previewHighlight(page, selector, label) {
|
|
|
6223
6239
|
}).catch(() => {});
|
|
6224
6240
|
}
|
|
6225
6241
|
/**
|
|
6226
|
-
* 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
|
|
6227
|
-
*/
|
|
6228
|
-
const AUTH_URL_PATTERN = /login|signin|sign-in|oauth|passport|sso|cas|challenge|captcha|verify|auth\b/i;
|
|
6229
|
-
/**
|
|
6230
6242
|
* 等待人工登录底层执行(独立导出供测试注入 fake page)。
|
|
6231
|
-
*
|
|
6232
|
-
* 1.
|
|
6233
|
-
*
|
|
6234
|
-
*
|
|
6235
|
-
*
|
|
6236
|
-
*
|
|
6243
|
+
* 完成判定(按优先级):
|
|
6244
|
+
* 1. **用户主动告知(主通道)**:pendingLoginWait 单槽状态机,面板常驻登录等待卡
|
|
6245
|
+
* 「我已完成登录」按钮 → resolveLoginWait(id),500ms 确认节拍;
|
|
6246
|
+
* 2. 显式确定性条件:successSelector(任一存活标签可见)/ successUrl(URL 正则,
|
|
6247
|
+
* 任一存活标签命中),pollIntervalMs 节拍多标签扫描,命中即把目标页切为命中页;
|
|
6248
|
+
* 3. 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
|
|
6249
|
+
*
|
|
6250
|
+
* 登录状态不再自动猜测:URL 认证特征/密码框消除/SPA 签名启发式对「登录表单
|
|
6251
|
+
* 隐藏不卸载(keep-alive/微前端)」「登录后 URL 仍含 auth 子串」的 SPA 会永久
|
|
6252
|
+
* 漏判(用户已登录仍空转到超时),已整体移除——登录完成由用户说了算。
|
|
6253
|
+
* 保留:存活标签感知(原标签关闭自愈迁移、新标签经 onNewPageDetected 注册)。
|
|
6237
6254
|
*/
|
|
6238
6255
|
async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
|
|
6239
6256
|
const instruction = typeof args.instruction === "string" && args.instruction.trim() ? args.instruction.trim() : "请在浏览器窗口中完成登录";
|
|
6240
6257
|
const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 12e4) || 12e4, 1e3), 6e5);
|
|
6241
6258
|
const pollIntervalMs = Math.min(Math.max(Number(args.pollIntervalMs ?? 2e3) || 2e3, 100), 5e3);
|
|
6259
|
+
const successSelector = typeof args.successSelector === "string" ? args.successSelector : "";
|
|
6260
|
+
const successUrl = typeof args.successUrl === "string" ? args.successUrl : "";
|
|
6242
6261
|
let currentPage = initialPage;
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
return await p.evaluate(() => !!document.querySelector("input[type=\"password\"]"));
|
|
6250
|
-
} catch {
|
|
6251
|
-
return false;
|
|
6252
|
-
}
|
|
6253
|
-
};
|
|
6254
|
-
const getPageSig = async (p) => {
|
|
6255
|
-
try {
|
|
6256
|
-
return await p.evaluate(() => {
|
|
6257
|
-
const body = document.body;
|
|
6258
|
-
return `${location.href}|${body ? body.innerText.length : 0}|${document.querySelectorAll("input,button,a").length}`;
|
|
6259
|
-
});
|
|
6260
|
-
} catch {
|
|
6261
|
-
try {
|
|
6262
|
-
return p.url();
|
|
6263
|
-
} catch {
|
|
6264
|
-
return "";
|
|
6265
|
-
}
|
|
6266
|
-
}
|
|
6267
|
-
};
|
|
6268
|
-
const startHasPassword = await checkHasPassword(initialPage);
|
|
6269
|
-
const startSig = args.successSelector || args.successUrl ? "" : await getPageSig(initialPage);
|
|
6262
|
+
const id = `login-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6263
|
+
setPendingLoginWait({
|
|
6264
|
+
id,
|
|
6265
|
+
instruction,
|
|
6266
|
+
startedAt: Date.now()
|
|
6267
|
+
});
|
|
6270
6268
|
const startedAt = Date.now();
|
|
6271
6269
|
let loggedIn = false;
|
|
6272
|
-
let detail = `等待登录超时(${timeoutMs}ms)
|
|
6273
|
-
|
|
6270
|
+
let detail = `等待登录超时(${timeoutMs}ms),用户未在面板确认登录完成`;
|
|
6271
|
+
let lastExplicitCheck = 0;
|
|
6274
6272
|
try {
|
|
6275
6273
|
const deadline = startedAt + timeoutMs;
|
|
6276
6274
|
while (Date.now() < deadline) {
|
|
6275
|
+
const pending = getPendingLoginWait();
|
|
6276
|
+
if (pending !== null && pending.id !== id) {
|
|
6277
|
+
detail = "登录等待已被新的等待请求取代";
|
|
6278
|
+
break;
|
|
6279
|
+
}
|
|
6280
|
+
if (pending !== null && pending.done) {
|
|
6281
|
+
loggedIn = true;
|
|
6282
|
+
detail = "用户已在面板确认完成登录";
|
|
6283
|
+
break;
|
|
6284
|
+
}
|
|
6277
6285
|
let candidatePages = [];
|
|
6278
6286
|
try {
|
|
6279
6287
|
const context = typeof currentPage.context === "function" ? currentPage.context() : null;
|
|
@@ -6298,77 +6306,35 @@ async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
|
|
|
6298
6306
|
isCurrentClosed = false;
|
|
6299
6307
|
}
|
|
6300
6308
|
}
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
currentPage = p;
|
|
6308
|
-
detail = `命中 successSelector: ${args.successSelector}`;
|
|
6309
|
-
break;
|
|
6310
|
-
}
|
|
6311
|
-
} catch {}
|
|
6312
|
-
} else if (args.successUrl) {
|
|
6313
|
-
const re = new RegExp(String(args.successUrl));
|
|
6314
|
-
const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
|
|
6315
|
-
for (const p of targets) try {
|
|
6316
|
-
const u = p.url();
|
|
6317
|
-
if (re.test(u)) {
|
|
6309
|
+
if ((successSelector !== "" || successUrl !== "") && Date.now() - lastExplicitCheck >= pollIntervalMs) {
|
|
6310
|
+
lastExplicitCheck = Date.now();
|
|
6311
|
+
const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
|
|
6312
|
+
for (const p of targets) try {
|
|
6313
|
+
if (successSelector !== "") {
|
|
6314
|
+
if (await p.locator(successSelector).first().isVisible()) {
|
|
6318
6315
|
loggedIn = true;
|
|
6319
6316
|
currentPage = p;
|
|
6320
|
-
detail = `命中
|
|
6317
|
+
detail = `命中 successSelector: ${successSelector}`;
|
|
6321
6318
|
break;
|
|
6322
6319
|
}
|
|
6323
|
-
}
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
try {
|
|
6329
|
-
curUrl = target.url();
|
|
6330
|
-
} catch {}
|
|
6331
|
-
if (curUrl && curUrl !== "about:blank") {
|
|
6332
|
-
if (curUrl !== startUrl) {
|
|
6333
|
-
if (!AUTH_URL_PATTERN.test(curUrl)) {
|
|
6334
|
-
loggedIn = true;
|
|
6335
|
-
currentPage = target;
|
|
6336
|
-
detail = `URL 离开登录页跳转至 ${curUrl}`;
|
|
6337
|
-
} else {
|
|
6338
|
-
const hasPwd = await checkHasPassword(target);
|
|
6339
|
-
if (startHasPassword && !hasPwd) {
|
|
6340
|
-
loggedIn = true;
|
|
6341
|
-
currentPage = target;
|
|
6342
|
-
detail = `登录密码输入框已消除,页面跳转至 ${curUrl}`;
|
|
6343
|
-
}
|
|
6344
|
-
}
|
|
6345
|
-
} else {
|
|
6346
|
-
const hasPwd = await checkHasPassword(target);
|
|
6347
|
-
if (startHasPassword && !hasPwd) {
|
|
6348
|
-
loggedIn = true;
|
|
6349
|
-
currentPage = target;
|
|
6350
|
-
detail = "登录密码输入框已消除(SPA 登录完成)";
|
|
6351
|
-
} else if (await getPageSig(target) !== startSig && (!startHasPassword || !hasPwd)) {
|
|
6352
|
-
loggedIn = true;
|
|
6353
|
-
currentPage = target;
|
|
6354
|
-
detail = "页面内容签名变更(SPA 登录完成)";
|
|
6355
|
-
}
|
|
6356
|
-
}
|
|
6357
|
-
}
|
|
6320
|
+
} else if (new RegExp(successUrl).test(p.url())) {
|
|
6321
|
+
loggedIn = true;
|
|
6322
|
+
currentPage = p;
|
|
6323
|
+
detail = `命中 successUrl: ${successUrl}`;
|
|
6324
|
+
break;
|
|
6358
6325
|
}
|
|
6359
|
-
}
|
|
6360
|
-
} catch (e) {
|
|
6361
|
-
detail = `检查登录状态出错: ${e instanceof Error ? e.message : String(e)}`;
|
|
6326
|
+
} catch {}
|
|
6362
6327
|
}
|
|
6363
6328
|
if (loggedIn) break;
|
|
6364
6329
|
try {
|
|
6365
|
-
await currentPage.waitForTimeout(
|
|
6330
|
+
await currentPage.waitForTimeout(500);
|
|
6366
6331
|
} catch {
|
|
6367
|
-
await sleep$1(
|
|
6332
|
+
await sleep$1(500);
|
|
6368
6333
|
}
|
|
6369
6334
|
}
|
|
6370
6335
|
} finally {
|
|
6371
|
-
|
|
6336
|
+
const pending = getPendingLoginWait();
|
|
6337
|
+
if (pending !== null && pending.id === id) clearPendingLoginWait();
|
|
6372
6338
|
}
|
|
6373
6339
|
return {
|
|
6374
6340
|
loggedIn,
|
|
@@ -6379,11 +6345,15 @@ async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
|
|
|
6379
6345
|
}
|
|
6380
6346
|
/**
|
|
6381
6347
|
* 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
|
|
6382
|
-
*
|
|
6383
|
-
*
|
|
6384
|
-
*
|
|
6348
|
+
* 用户在浏览器窗口完成登录(输入/扫码/双因素)后,在实时画面面板的**常驻登录
|
|
6349
|
+
* 等待卡**上点「我已完成登录」主动告知,工具立即返回(500ms 确认节拍)。
|
|
6350
|
+
* 亦支持显式确定性条件 successSelector/successUrl(多标签扫描自动判定)。
|
|
6351
|
+
* 不做任何登录状态启发式猜测。超时返回 loggedIn:false(附 waitedMs 与
|
|
6352
|
+
* detail,不抛错)。无面板环境(MCP 独立模式)必须显式给条件,否则 fail-fast。
|
|
6385
6353
|
*/
|
|
6386
6354
|
async function waitForLoginTool(config, args) {
|
|
6355
|
+
const hasExplicit = typeof args.successSelector === "string" && args.successSelector !== "" || typeof args.successUrl === "string" && args.successUrl !== "";
|
|
6356
|
+
if (!isInteractivePanelPresent() && !hasExplicit) throw new Error("等待人工登录失败: 当前环境无交互面板,用户无法点「我已完成登录」确认。请显式提供 successSelector(登录成功后出现的元素,如 \"#avatar\")或 successUrl(登录成功后 URL 应匹配的正则)后重试");
|
|
6387
6357
|
const page = await getPage(config);
|
|
6388
6358
|
const state = stateOf();
|
|
6389
6359
|
const result = await runWaitForLoginOnPage(page, args, (newPage) => {
|
|
@@ -9592,13 +9562,13 @@ function getCollaborationTools(config, timeout, stepImpls) {
|
|
|
9592
9562
|
return [
|
|
9593
9563
|
definePlainTool({
|
|
9594
9564
|
name: "browser_wait_for_login",
|
|
9595
|
-
description: "等待人工登录完成。agent
|
|
9565
|
+
description: "等待人工登录完成。agent 在登录页调用本工具后,用户在浏览器窗口完成登录(有头模式 headless:false;配 userDataDir 登录态会持久化),然后在实时画面面板的常驻登录等待卡上点「我已完成登录」主动告知,工具立即返回(500ms 确认节拍)——不猜测登录状态,完成与否由用户说了算。亦可显式给 successSelector(登录后出现的元素,如 '#avatar')或 successUrl(登录后 URL 正则)作为确定性自动条件(多标签扫描,轮询间隔 2 秒)。超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。注意:无面板环境(独立 MCP 模式)用户无法点按钮确认,必须显式给 successSelector 或 successUrl,否则调用直接报错。",
|
|
9596
9566
|
parameters: {
|
|
9597
9567
|
type: "object",
|
|
9598
9568
|
properties: {
|
|
9599
9569
|
instruction: {
|
|
9600
9570
|
type: "string",
|
|
9601
|
-
description: "
|
|
9571
|
+
description: "可选:给用户看的提示说明,展示在面板登录等待卡上(默认「请在浏览器窗口中完成登录」)。"
|
|
9602
9572
|
},
|
|
9603
9573
|
timeoutMs: {
|
|
9604
9574
|
type: "integer",
|
|
@@ -9606,11 +9576,11 @@ function getCollaborationTools(config, timeout, stepImpls) {
|
|
|
9606
9576
|
},
|
|
9607
9577
|
successSelector: {
|
|
9608
9578
|
type: "string",
|
|
9609
|
-
description: "
|
|
9579
|
+
description: "可选:登录成功后应出现的 CSS 选择器(确定性自动条件,任一标签可见即完成)。"
|
|
9610
9580
|
},
|
|
9611
9581
|
successUrl: {
|
|
9612
9582
|
type: "string",
|
|
9613
|
-
description: "
|
|
9583
|
+
description: "可选:登录成功后 URL 应匹配的正则(确定性自动条件)。"
|
|
9614
9584
|
}
|
|
9615
9585
|
},
|
|
9616
9586
|
additionalProperties: false
|
|
@@ -10588,6 +10558,24 @@ function registerBrowserApi(ctx, config, base) {
|
|
|
10588
10558
|
sendJson(res, 200, { confirm: getPendingConfirm() });
|
|
10589
10559
|
return;
|
|
10590
10560
|
}
|
|
10561
|
+
if (path === `${base}/login`) {
|
|
10562
|
+
if ((req.method ?? "GET") === "POST") {
|
|
10563
|
+
const body = await readJsonBody(req).catch(() => void 0);
|
|
10564
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
10565
|
+
if (!id) {
|
|
10566
|
+
sendText(res, 400, "id required");
|
|
10567
|
+
return;
|
|
10568
|
+
}
|
|
10569
|
+
const resolved = resolveLoginWait(id);
|
|
10570
|
+
sendJson(res, resolved ? 200 : 409, {
|
|
10571
|
+
resolved,
|
|
10572
|
+
id
|
|
10573
|
+
});
|
|
10574
|
+
return;
|
|
10575
|
+
}
|
|
10576
|
+
sendJson(res, 200, { loginWait: getPendingLoginWait() });
|
|
10577
|
+
return;
|
|
10578
|
+
}
|
|
10591
10579
|
if (path === `${base}/pick`) {
|
|
10592
10580
|
if ((req.method ?? "GET") === "POST") {
|
|
10593
10581
|
const body = await readJsonBody(req).catch(() => void 0);
|
package/lib/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -184,8 +184,8 @@ const BROWSER_DEFAULT_CONFIG = {
|
|
|
184
184
|
};
|
|
185
185
|
//#endregion
|
|
186
186
|
//#region lib/types/lib/prompts.js
|
|
187
|
-
const FULL_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话,支持多标签页与多配置文件。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),页内打分匹配并自动注册 ref,一次调用替代 快照→查找→点击 三轮;零命中报错会附最相近候选。需要精确控制时再用 selector(CSS)/ref(aria 快照)。打开页面后可先 browser_snapshot 读文本与链接、browser_elements 读表单结构(含关联 label 与展开的下拉选项);browser_find(query 传名称,只返回命中 ref+区域上下文,大页面最省 token;零命中会给相近候选)适合多目标排查,需要全页结构时再用 browser_snapshot { mode: \"aria\" }(可交互元素带 ref=eN,click/type/select/press 直接传 ref 代替 CSS 选择器);下拉选择(原生 select 与 antd/element 组件库)用 browser_select 传 selector/ref/what + option 文本一步完成;固定小序列(点击→等待→输入→提交)用 browser_batch 一次执行减少往返;标签管理用 browser_tabs;下载用 browser_download(wait=true 可等新下载);登录态用 browser_cookies。做自动化测试时:用 browser_wait_for 显式等待页面稳定/元素出现,用 browser_assert 断言(失败自动截图;visual={baseline} 可做视觉回归,先用 browser_screenshot { baseline } 建基线),用 browser_network 检查接口调用与 4xx/5xx,跑完一轮可用 browser_record save 保存操作序列、browser_replay 回放做回归(选择器失效自动按指纹自愈;vars/data 支持参数化与数据驱动)。每次 replay/batch 都记入运行历史,browser_report list/export 可查看与导出回归报告。抓取结构化数据用 browser_extract(fields 定义 CSS 字段,可翻页,可导出 CSV/JSONL)。遇到需要登录的网站:先 browser_open
|
|
188
|
-
const CORE_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话(toolset=core:已精简工具集)。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),一次调用替代快照→查找→点击;零命中报错附最相近候选。需要精确控制时再用 selector/ref:打开页面后用 browser_snapshot 读内容(scope 限定区域、since 增量重读省 token),browser_find(query 传名称,返回 ref),点击/输入/按键/下拉用 browser_click/browser_type/browser_press/browser_select(selector 或 ref);固定小序列用 browser_batch;等待用 browser_wait/browser_wait_for,断言用 browser_assert;接口检查用 browser_network;标签 browser_tabs;下载/上传 browser_download/browser_upload。需要人工:登录用 browser_wait_for_login,验证码/二次确认用 browser_pause_for_human,敏感动作(提交/删除/支付)前用 browser_confirm。环境异常先跑 browser_doctor 诊断。录制/回放/报告/抽取/profile/隔离 context 等工具当前未注册(toolset=core),需要时把配置改为 toolset: all 并重启 DSH。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
187
|
+
const FULL_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话,支持多标签页与多配置文件。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),页内打分匹配并自动注册 ref,一次调用替代 快照→查找→点击 三轮;零命中报错会附最相近候选。需要精确控制时再用 selector(CSS)/ref(aria 快照)。打开页面后可先 browser_snapshot 读文本与链接、browser_elements 读表单结构(含关联 label 与展开的下拉选项);browser_find(query 传名称,只返回命中 ref+区域上下文,大页面最省 token;零命中会给相近候选)适合多目标排查,需要全页结构时再用 browser_snapshot { mode: \"aria\" }(可交互元素带 ref=eN,click/type/select/press 直接传 ref 代替 CSS 选择器);下拉选择(原生 select 与 antd/element 组件库)用 browser_select 传 selector/ref/what + option 文本一步完成;固定小序列(点击→等待→输入→提交)用 browser_batch 一次执行减少往返;标签管理用 browser_tabs;下载用 browser_download(wait=true 可等新下载);登录态用 browser_cookies。做自动化测试时:用 browser_wait_for 显式等待页面稳定/元素出现,用 browser_assert 断言(失败自动截图;visual={baseline} 可做视觉回归,先用 browser_screenshot { baseline } 建基线),用 browser_network 检查接口调用与 4xx/5xx,跑完一轮可用 browser_record save 保存操作序列、browser_replay 回放做回归(选择器失效自动按指纹自愈;vars/data 支持参数化与数据驱动)。每次 replay/batch 都记入运行历史,browser_report list/export 可查看与导出回归报告。抓取结构化数据用 browser_extract(fields 定义 CSS 字段,可翻页,可导出 CSV/JSONL)。遇到需要登录的网站:先 browser_open 打开登录页,告知用户在浏览器窗口完成登录后点面板登录卡上的「我已完成登录」,再调用 browser_wait_for_login 等待(用户点按钮即返回,不自动猜测登录状态;也可给 successSelector/successUrl 作确定性自动条件;无面板环境必须给其一,否则调用会报错);验证码/二次确认等更广的「人补一步」场景用 browser_pause_for_human(提示语会显示在实时画面面板);配置 userDataDir 时登录态会持久化。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
188
|
+
const CORE_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话(toolset=core:已精简工具集)。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),一次调用替代快照→查找→点击;零命中报错附最相近候选。需要精确控制时再用 selector/ref:打开页面后用 browser_snapshot 读内容(scope 限定区域、since 增量重读省 token),browser_find(query 传名称,返回 ref),点击/输入/按键/下拉用 browser_click/browser_type/browser_press/browser_select(selector 或 ref);固定小序列用 browser_batch;等待用 browser_wait/browser_wait_for,断言用 browser_assert;接口检查用 browser_network;标签 browser_tabs;下载/上传 browser_download/browser_upload。需要人工:登录用 browser_wait_for_login(用户在面板点「我已完成登录」即返回;无面板环境必须给 successSelector/successUrl,否则调用报错),验证码/二次确认用 browser_pause_for_human,敏感动作(提交/删除/支付)前用 browser_confirm。环境异常先跑 browser_doctor 诊断。录制/回放/报告/抽取/profile/隔离 context 等工具当前未注册(toolset=core),需要时把配置改为 toolset: all 并重启 DSH。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
189
189
|
/** confirmMode 附加指引(与 DSH 入口同款语义,manual 面向宿主审批、auto 说明硬闸)。 */
|
|
190
190
|
function confirmGuidanceOf(confirmMode) {
|
|
191
191
|
if (confirmMode === "manual") return "本环境 confirmMode 为 manual:凡提交/删除/支付等不可逆或影响线上数据的敏感动作,执行前必须先调用 browser_confirm(reason 说明操作与影响,等待用户在实时画面面板点允许/拒绝;拒绝或超时即放弃该动作,不得换路径绕过)。";
|
|
@@ -528,6 +528,16 @@ function getPendingConfirm() {
|
|
|
528
528
|
function clearPendingConfirm() {
|
|
529
529
|
pendingConfirm = null;
|
|
530
530
|
}
|
|
531
|
+
let pendingLoginWait = null;
|
|
532
|
+
function setPendingLoginWait(entry) {
|
|
533
|
+
pendingLoginWait = entry;
|
|
534
|
+
}
|
|
535
|
+
function getPendingLoginWait() {
|
|
536
|
+
return pendingLoginWait;
|
|
537
|
+
}
|
|
538
|
+
function clearPendingLoginWait() {
|
|
539
|
+
pendingLoginWait = null;
|
|
540
|
+
}
|
|
531
541
|
let pendingPick = null;
|
|
532
542
|
function setPendingPick(entry) {
|
|
533
543
|
pendingPick = entry;
|
|
@@ -6132,56 +6142,49 @@ function previewHighlight(page, selector, label) {
|
|
|
6132
6142
|
}).catch(() => {});
|
|
6133
6143
|
}
|
|
6134
6144
|
/**
|
|
6135
|
-
* 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
|
|
6136
|
-
*/
|
|
6137
|
-
const AUTH_URL_PATTERN = /login|signin|sign-in|oauth|passport|sso|cas|challenge|captcha|verify|auth\b/i;
|
|
6138
|
-
/**
|
|
6139
6145
|
* 等待人工登录底层执行(独立导出供测试注入 fake page)。
|
|
6140
|
-
*
|
|
6141
|
-
* 1.
|
|
6142
|
-
*
|
|
6143
|
-
*
|
|
6144
|
-
*
|
|
6145
|
-
*
|
|
6146
|
+
* 完成判定(按优先级):
|
|
6147
|
+
* 1. **用户主动告知(主通道)**:pendingLoginWait 单槽状态机,面板常驻登录等待卡
|
|
6148
|
+
* 「我已完成登录」按钮 → resolveLoginWait(id),500ms 确认节拍;
|
|
6149
|
+
* 2. 显式确定性条件:successSelector(任一存活标签可见)/ successUrl(URL 正则,
|
|
6150
|
+
* 任一存活标签命中),pollIntervalMs 节拍多标签扫描,命中即把目标页切为命中页;
|
|
6151
|
+
* 3. 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
|
|
6152
|
+
*
|
|
6153
|
+
* 登录状态不再自动猜测:URL 认证特征/密码框消除/SPA 签名启发式对「登录表单
|
|
6154
|
+
* 隐藏不卸载(keep-alive/微前端)」「登录后 URL 仍含 auth 子串」的 SPA 会永久
|
|
6155
|
+
* 漏判(用户已登录仍空转到超时),已整体移除——登录完成由用户说了算。
|
|
6156
|
+
* 保留:存活标签感知(原标签关闭自愈迁移、新标签经 onNewPageDetected 注册)。
|
|
6146
6157
|
*/
|
|
6147
6158
|
async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
|
|
6148
|
-
typeof args.instruction === "string" && args.instruction.trim()
|
|
6159
|
+
const instruction = typeof args.instruction === "string" && args.instruction.trim() ? args.instruction.trim() : "请在浏览器窗口中完成登录";
|
|
6149
6160
|
const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 12e4) || 12e4, 1e3), 6e5);
|
|
6150
6161
|
const pollIntervalMs = Math.min(Math.max(Number(args.pollIntervalMs ?? 2e3) || 2e3, 100), 5e3);
|
|
6162
|
+
const successSelector = typeof args.successSelector === "string" ? args.successSelector : "";
|
|
6163
|
+
const successUrl = typeof args.successUrl === "string" ? args.successUrl : "";
|
|
6151
6164
|
let currentPage = initialPage;
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
return await p.evaluate(() => !!document.querySelector("input[type=\"password\"]"));
|
|
6159
|
-
} catch {
|
|
6160
|
-
return false;
|
|
6161
|
-
}
|
|
6162
|
-
};
|
|
6163
|
-
const getPageSig = async (p) => {
|
|
6164
|
-
try {
|
|
6165
|
-
return await p.evaluate(() => {
|
|
6166
|
-
const body = document.body;
|
|
6167
|
-
return `${location.href}|${body ? body.innerText.length : 0}|${document.querySelectorAll("input,button,a").length}`;
|
|
6168
|
-
});
|
|
6169
|
-
} catch {
|
|
6170
|
-
try {
|
|
6171
|
-
return p.url();
|
|
6172
|
-
} catch {
|
|
6173
|
-
return "";
|
|
6174
|
-
}
|
|
6175
|
-
}
|
|
6176
|
-
};
|
|
6177
|
-
const startHasPassword = await checkHasPassword(initialPage);
|
|
6178
|
-
const startSig = args.successSelector || args.successUrl ? "" : await getPageSig(initialPage);
|
|
6165
|
+
const id = `login-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6166
|
+
setPendingLoginWait({
|
|
6167
|
+
id,
|
|
6168
|
+
instruction,
|
|
6169
|
+
startedAt: Date.now()
|
|
6170
|
+
});
|
|
6179
6171
|
const startedAt = Date.now();
|
|
6180
6172
|
let loggedIn = false;
|
|
6181
|
-
let detail = `等待登录超时(${timeoutMs}ms)
|
|
6173
|
+
let detail = `等待登录超时(${timeoutMs}ms),用户未在面板确认登录完成`;
|
|
6174
|
+
let lastExplicitCheck = 0;
|
|
6182
6175
|
try {
|
|
6183
6176
|
const deadline = startedAt + timeoutMs;
|
|
6184
6177
|
while (Date.now() < deadline) {
|
|
6178
|
+
const pending = getPendingLoginWait();
|
|
6179
|
+
if (pending !== null && pending.id !== id) {
|
|
6180
|
+
detail = "登录等待已被新的等待请求取代";
|
|
6181
|
+
break;
|
|
6182
|
+
}
|
|
6183
|
+
if (pending !== null && pending.done) {
|
|
6184
|
+
loggedIn = true;
|
|
6185
|
+
detail = "用户已在面板确认完成登录";
|
|
6186
|
+
break;
|
|
6187
|
+
}
|
|
6185
6188
|
let candidatePages = [];
|
|
6186
6189
|
try {
|
|
6187
6190
|
const context = typeof currentPage.context === "function" ? currentPage.context() : null;
|
|
@@ -6206,76 +6209,36 @@ async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
|
|
|
6206
6209
|
isCurrentClosed = false;
|
|
6207
6210
|
}
|
|
6208
6211
|
}
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
currentPage = p;
|
|
6216
|
-
detail = `命中 successSelector: ${args.successSelector}`;
|
|
6217
|
-
break;
|
|
6218
|
-
}
|
|
6219
|
-
} catch {}
|
|
6220
|
-
} else if (args.successUrl) {
|
|
6221
|
-
const re = new RegExp(String(args.successUrl));
|
|
6222
|
-
const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
|
|
6223
|
-
for (const p of targets) try {
|
|
6224
|
-
const u = p.url();
|
|
6225
|
-
if (re.test(u)) {
|
|
6212
|
+
if ((successSelector !== "" || successUrl !== "") && Date.now() - lastExplicitCheck >= pollIntervalMs) {
|
|
6213
|
+
lastExplicitCheck = Date.now();
|
|
6214
|
+
const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
|
|
6215
|
+
for (const p of targets) try {
|
|
6216
|
+
if (successSelector !== "") {
|
|
6217
|
+
if (await p.locator(successSelector).first().isVisible()) {
|
|
6226
6218
|
loggedIn = true;
|
|
6227
6219
|
currentPage = p;
|
|
6228
|
-
detail = `命中
|
|
6220
|
+
detail = `命中 successSelector: ${successSelector}`;
|
|
6229
6221
|
break;
|
|
6230
6222
|
}
|
|
6231
|
-
}
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
try {
|
|
6237
|
-
curUrl = target.url();
|
|
6238
|
-
} catch {}
|
|
6239
|
-
if (curUrl && curUrl !== "about:blank") {
|
|
6240
|
-
if (curUrl !== startUrl) {
|
|
6241
|
-
if (!AUTH_URL_PATTERN.test(curUrl)) {
|
|
6242
|
-
loggedIn = true;
|
|
6243
|
-
currentPage = target;
|
|
6244
|
-
detail = `URL 离开登录页跳转至 ${curUrl}`;
|
|
6245
|
-
} else {
|
|
6246
|
-
const hasPwd = await checkHasPassword(target);
|
|
6247
|
-
if (startHasPassword && !hasPwd) {
|
|
6248
|
-
loggedIn = true;
|
|
6249
|
-
currentPage = target;
|
|
6250
|
-
detail = `登录密码输入框已消除,页面跳转至 ${curUrl}`;
|
|
6251
|
-
}
|
|
6252
|
-
}
|
|
6253
|
-
} else {
|
|
6254
|
-
const hasPwd = await checkHasPassword(target);
|
|
6255
|
-
if (startHasPassword && !hasPwd) {
|
|
6256
|
-
loggedIn = true;
|
|
6257
|
-
currentPage = target;
|
|
6258
|
-
detail = "登录密码输入框已消除(SPA 登录完成)";
|
|
6259
|
-
} else if (await getPageSig(target) !== startSig && (!startHasPassword || !hasPwd)) {
|
|
6260
|
-
loggedIn = true;
|
|
6261
|
-
currentPage = target;
|
|
6262
|
-
detail = "页面内容签名变更(SPA 登录完成)";
|
|
6263
|
-
}
|
|
6264
|
-
}
|
|
6265
|
-
}
|
|
6223
|
+
} else if (new RegExp(successUrl).test(p.url())) {
|
|
6224
|
+
loggedIn = true;
|
|
6225
|
+
currentPage = p;
|
|
6226
|
+
detail = `命中 successUrl: ${successUrl}`;
|
|
6227
|
+
break;
|
|
6266
6228
|
}
|
|
6267
|
-
}
|
|
6268
|
-
} catch (e) {
|
|
6269
|
-
detail = `检查登录状态出错: ${e instanceof Error ? e.message : String(e)}`;
|
|
6229
|
+
} catch {}
|
|
6270
6230
|
}
|
|
6271
6231
|
if (loggedIn) break;
|
|
6272
6232
|
try {
|
|
6273
|
-
await currentPage.waitForTimeout(
|
|
6233
|
+
await currentPage.waitForTimeout(500);
|
|
6274
6234
|
} catch {
|
|
6275
|
-
await sleep$1(
|
|
6235
|
+
await sleep$1(500);
|
|
6276
6236
|
}
|
|
6277
6237
|
}
|
|
6278
|
-
} finally {
|
|
6238
|
+
} finally {
|
|
6239
|
+
const pending = getPendingLoginWait();
|
|
6240
|
+
if (pending !== null && pending.id === id) clearPendingLoginWait();
|
|
6241
|
+
}
|
|
6279
6242
|
return {
|
|
6280
6243
|
loggedIn,
|
|
6281
6244
|
waitedMs: Date.now() - startedAt,
|
|
@@ -6285,11 +6248,15 @@ async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
|
|
|
6285
6248
|
}
|
|
6286
6249
|
/**
|
|
6287
6250
|
* 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
|
|
6288
|
-
*
|
|
6289
|
-
*
|
|
6290
|
-
*
|
|
6251
|
+
* 用户在浏览器窗口完成登录(输入/扫码/双因素)后,在实时画面面板的**常驻登录
|
|
6252
|
+
* 等待卡**上点「我已完成登录」主动告知,工具立即返回(500ms 确认节拍)。
|
|
6253
|
+
* 亦支持显式确定性条件 successSelector/successUrl(多标签扫描自动判定)。
|
|
6254
|
+
* 不做任何登录状态启发式猜测。超时返回 loggedIn:false(附 waitedMs 与
|
|
6255
|
+
* detail,不抛错)。无面板环境(MCP 独立模式)必须显式给条件,否则 fail-fast。
|
|
6291
6256
|
*/
|
|
6292
6257
|
async function waitForLoginTool(config, args) {
|
|
6258
|
+
const hasExplicit = typeof args.successSelector === "string" && args.successSelector !== "" || typeof args.successUrl === "string" && args.successUrl !== "";
|
|
6259
|
+
if (!isInteractivePanelPresent() && !hasExplicit) throw new Error("等待人工登录失败: 当前环境无交互面板,用户无法点「我已完成登录」确认。请显式提供 successSelector(登录成功后出现的元素,如 \"#avatar\")或 successUrl(登录成功后 URL 应匹配的正则)后重试");
|
|
6293
6260
|
const page = await getPage(config);
|
|
6294
6261
|
const state = stateOf();
|
|
6295
6262
|
const result = await runWaitForLoginOnPage(page, args, (newPage) => {
|
|
@@ -9468,13 +9435,13 @@ function getCollaborationTools(config, timeout, stepImpls) {
|
|
|
9468
9435
|
return [
|
|
9469
9436
|
definePlainTool({
|
|
9470
9437
|
name: "browser_wait_for_login",
|
|
9471
|
-
description: "等待人工登录完成。agent
|
|
9438
|
+
description: "等待人工登录完成。agent 在登录页调用本工具后,用户在浏览器窗口完成登录(有头模式 headless:false;配 userDataDir 登录态会持久化),然后在实时画面面板的常驻登录等待卡上点「我已完成登录」主动告知,工具立即返回(500ms 确认节拍)——不猜测登录状态,完成与否由用户说了算。亦可显式给 successSelector(登录后出现的元素,如 '#avatar')或 successUrl(登录后 URL 正则)作为确定性自动条件(多标签扫描,轮询间隔 2 秒)。超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。注意:无面板环境(独立 MCP 模式)用户无法点按钮确认,必须显式给 successSelector 或 successUrl,否则调用直接报错。",
|
|
9472
9439
|
parameters: {
|
|
9473
9440
|
type: "object",
|
|
9474
9441
|
properties: {
|
|
9475
9442
|
instruction: {
|
|
9476
9443
|
type: "string",
|
|
9477
|
-
description: "
|
|
9444
|
+
description: "可选:给用户看的提示说明,展示在面板登录等待卡上(默认「请在浏览器窗口中完成登录」)。"
|
|
9478
9445
|
},
|
|
9479
9446
|
timeoutMs: {
|
|
9480
9447
|
type: "integer",
|
|
@@ -9482,11 +9449,11 @@ function getCollaborationTools(config, timeout, stepImpls) {
|
|
|
9482
9449
|
},
|
|
9483
9450
|
successSelector: {
|
|
9484
9451
|
type: "string",
|
|
9485
|
-
description: "
|
|
9452
|
+
description: "可选:登录成功后应出现的 CSS 选择器(确定性自动条件,任一标签可见即完成)。"
|
|
9486
9453
|
},
|
|
9487
9454
|
successUrl: {
|
|
9488
9455
|
type: "string",
|
|
9489
|
-
description: "
|
|
9456
|
+
description: "可选:登录成功后 URL 应匹配的正则(确定性自动条件)。"
|
|
9490
9457
|
}
|
|
9491
9458
|
},
|
|
9492
9459
|
additionalProperties: false
|
|
@@ -10502,6 +10469,12 @@ async function main() {
|
|
|
10502
10469
|
await server.connect(transport);
|
|
10503
10470
|
console.error(`dsh-browser-mcp: stdio server ready (toolset=${config.toolset}, headless=${config.headless}, profile=${config.userDataDir || "ephemeral"})`);
|
|
10504
10471
|
}
|
|
10505
|
-
if (process.argv[1] && import.meta.url ===
|
|
10472
|
+
if (process.argv[1] && import.meta.url === (() => {
|
|
10473
|
+
try {
|
|
10474
|
+
return pathToFileURL(realpathSync(process.argv[1])).href;
|
|
10475
|
+
} catch {
|
|
10476
|
+
return "";
|
|
10477
|
+
}
|
|
10478
|
+
})()) main();
|
|
10506
10479
|
//#endregion
|
|
10507
10480
|
export { collectToolDefinitions, configFromEnv, createBrowserMcpServer, main, resolveToolAnnotations };
|
|
@@ -21,6 +21,19 @@ export declare function getPendingConfirm(): PendingConfirm | null;
|
|
|
21
21
|
export declare function clearPendingConfirm(): void;
|
|
22
22
|
/** 面板提交审批决策;id 不匹配(已被新审批取代/超时清理)时返回 false。 */
|
|
23
23
|
export declare function resolveConfirm(id: string, decision: "allowed" | "denied"): boolean;
|
|
24
|
+
/** 一次等待人工登录的请求(browser_wait_for_login 等待期间供面板展示与确认)。 */
|
|
25
|
+
export interface PendingLoginWait {
|
|
26
|
+
id: string;
|
|
27
|
+
instruction: string;
|
|
28
|
+
startedAt: number;
|
|
29
|
+
/** 用户点「我已完成登录」后置 true。 */
|
|
30
|
+
done?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function setPendingLoginWait(entry: PendingLoginWait): void;
|
|
33
|
+
export declare function getPendingLoginWait(): PendingLoginWait | null;
|
|
34
|
+
export declare function clearPendingLoginWait(): void;
|
|
35
|
+
/** 面板提交「用户已完成登录」;id 不匹配(已被新请求取代/超时清理)时返回 false。 */
|
|
36
|
+
export declare function resolveLoginWait(id: string): boolean;
|
|
24
37
|
/** 一次待点选的教学请求(browser_pick_element 等待期间供面板展示与提交)。 */
|
|
25
38
|
export interface PendingPick {
|
|
26
39
|
id: string;
|
|
@@ -5,10 +5,6 @@ import type { BrowserConfig } from "../types.js";
|
|
|
5
5
|
* ~1.5 秒。异步不阻塞(fire-and-forget,任何失败静默),不影响操作时序。
|
|
6
6
|
*/
|
|
7
7
|
export declare function previewHighlight(page: Page | null, selector: string, label: string): void;
|
|
8
|
-
/**
|
|
9
|
-
* 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
|
|
10
|
-
*/
|
|
11
|
-
export declare const AUTH_URL_PATTERN: RegExp;
|
|
12
8
|
export interface WaitForLoginResult {
|
|
13
9
|
loggedIn: boolean;
|
|
14
10
|
waitedMs: number;
|
|
@@ -17,19 +13,26 @@ export interface WaitForLoginResult {
|
|
|
17
13
|
}
|
|
18
14
|
/**
|
|
19
15
|
* 等待人工登录底层执行(独立导出供测试注入 fake page)。
|
|
20
|
-
*
|
|
21
|
-
* 1.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
16
|
+
* 完成判定(按优先级):
|
|
17
|
+
* 1. **用户主动告知(主通道)**:pendingLoginWait 单槽状态机,面板常驻登录等待卡
|
|
18
|
+
* 「我已完成登录」按钮 → resolveLoginWait(id),500ms 确认节拍;
|
|
19
|
+
* 2. 显式确定性条件:successSelector(任一存活标签可见)/ successUrl(URL 正则,
|
|
20
|
+
* 任一存活标签命中),pollIntervalMs 节拍多标签扫描,命中即把目标页切为命中页;
|
|
21
|
+
* 3. 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
|
|
22
|
+
*
|
|
23
|
+
* 登录状态不再自动猜测:URL 认证特征/密码框消除/SPA 签名启发式对「登录表单
|
|
24
|
+
* 隐藏不卸载(keep-alive/微前端)」「登录后 URL 仍含 auth 子串」的 SPA 会永久
|
|
25
|
+
* 漏判(用户已登录仍空转到超时),已整体移除——登录完成由用户说了算。
|
|
26
|
+
* 保留:存活标签感知(原标签关闭自愈迁移、新标签经 onNewPageDetected 注册)。
|
|
26
27
|
*/
|
|
27
28
|
export declare function runWaitForLoginOnPage(initialPage: Page, args: Record<string, any>, onNewPageDetected?: (page: Page) => void): Promise<WaitForLoginResult>;
|
|
28
29
|
/**
|
|
29
30
|
* 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* 用户在浏览器窗口完成登录(输入/扫码/双因素)后,在实时画面面板的**常驻登录
|
|
32
|
+
* 等待卡**上点「我已完成登录」主动告知,工具立即返回(500ms 确认节拍)。
|
|
33
|
+
* 亦支持显式确定性条件 successSelector/successUrl(多标签扫描自动判定)。
|
|
34
|
+
* 不做任何登录状态启发式猜测。超时返回 loggedIn:false(附 waitedMs 与
|
|
35
|
+
* detail,不抛错)。无面板环境(MCP 独立模式)必须显式给条件,否则 fail-fast。
|
|
33
36
|
*/
|
|
34
37
|
export declare function waitForLoginTool(config: BrowserConfig, args: Record<string, any>): Promise<{
|
|
35
38
|
url: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserOp, BrowserStatus, LatestRun, NavHistoryItem, PendingConfirm, PendingPick, RecordingDetail, RecordingRow } from "./types.js";
|
|
1
|
+
import type { BrowserOp, BrowserStatus, LatestRun, NavHistoryItem, PendingConfirm, PendingLoginWait, PendingPick, RecordingDetail, RecordingRow } from "./types.js";
|
|
2
2
|
/** 获取插件后端 API 基路径 (默认为 /browser) */
|
|
3
3
|
export declare function getBasePath(): Promise<string>;
|
|
4
4
|
export declare function fetchStatus(base: string): Promise<BrowserStatus | null>;
|
|
@@ -11,6 +11,9 @@ export declare function fetchLatestRun(base: string): Promise<{
|
|
|
11
11
|
export declare function fetchPendingConfirm(base: string): Promise<PendingConfirm | null>;
|
|
12
12
|
export declare function submitConfirmDecision(base: string, id: string, decision: "allowed" | "denied"): Promise<boolean>;
|
|
13
13
|
export declare function fetchPendingPick(base: string): Promise<PendingPick | null>;
|
|
14
|
+
export declare function fetchPendingLoginWait(base: string): Promise<PendingLoginWait | null>;
|
|
15
|
+
/** 提交「用户已完成登录」确认。 */
|
|
16
|
+
export declare function submitLoginDone(base: string, id: string): Promise<boolean>;
|
|
14
17
|
/** 提交点选坐标(x/y 为截图位图像素坐标,imgW/imgH 为截图尺寸;服务端换算成视口 CSS 坐标)。 */
|
|
15
18
|
export declare function submitPick(base: string, id: string, x: number, y: number, imgW: number, imgH: number): Promise<boolean>;
|
|
16
19
|
export declare function triggerRerun(base: string, source: string, retryOf?: string): Promise<{
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PendingLoginWait } from "../types.js";
|
|
2
|
+
interface LoginWaitCardProps {
|
|
3
|
+
loginWait: PendingLoginWait;
|
|
4
|
+
onDone: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* browser_wait_for_login 等待期间常驻悬浮于 FAB 上方(面板收起也可见):
|
|
8
|
+
* 用户此时在浏览器窗口里登录,可能不看 DSH 面板,卡片必须始终露出。
|
|
9
|
+
* 「我已完成登录」→ POST /browser/login → 工具立即返回 loggedIn:true。
|
|
10
|
+
*/
|
|
11
|
+
export declare function LoginWaitCard({ loginWait, onDone }: LoginWaitCardProps): import("react").DetailedReactHTMLElement<{
|
|
12
|
+
style: {
|
|
13
|
+
position: "fixed";
|
|
14
|
+
right: string;
|
|
15
|
+
bottom: string;
|
|
16
|
+
width: string;
|
|
17
|
+
padding: string;
|
|
18
|
+
borderRadius: string;
|
|
19
|
+
border: string;
|
|
20
|
+
background: string;
|
|
21
|
+
color: "#15803d";
|
|
22
|
+
fontSize: string;
|
|
23
|
+
lineHeight: string;
|
|
24
|
+
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.18)";
|
|
25
|
+
pointerEvents: "auto";
|
|
26
|
+
};
|
|
27
|
+
title: string;
|
|
28
|
+
}, HTMLElement>;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=LoginWaitCard.d.ts.map
|
|
@@ -56,6 +56,13 @@ export interface PendingPick {
|
|
|
56
56
|
instruction: string;
|
|
57
57
|
startedAt: number;
|
|
58
58
|
}
|
|
59
|
+
/** 等待人工登录请求(browser_wait_for_login 等待期间轮询到,用户点「我已完成登录」)。 */
|
|
60
|
+
export interface PendingLoginWait {
|
|
61
|
+
id: string;
|
|
62
|
+
instruction: string;
|
|
63
|
+
startedAt: number;
|
|
64
|
+
done?: boolean;
|
|
65
|
+
}
|
|
59
66
|
export interface LatestRun {
|
|
60
67
|
runId: string;
|
|
61
68
|
ts: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BrowserConfig } from "../browser/types.js";
|
|
2
|
-
declare const FULL_TOOL_PROMPT = "\u6D4F\u89C8\u5668\u5DE5\u5177\u64CD\u4F5C\u4E00\u4E2A\u5171\u4EAB\u6D4F\u89C8\u5668\u4F1A\u8BDD,\u652F\u6301\u591A\u6807\u7B7E\u9875\u4E0E\u591A\u914D\u7F6E\u6587\u4EF6\u3002\u5B9A\u4F4D\u64CD\u4F5C\u5143\u7D20\u6700\u7701\u4E8B\u7684\u65B9\u5F0F\u662F\u610F\u56FE\u8BED\u4E49\u5B9A\u4F4D:browser_click/browser_type/browser_select \u76F4\u63A5\u4F20 what(\u5143\u7D20\u540D\u79F0/label,\u5982 what:'\u767B\u5F55'),\u9875\u5185\u6253\u5206\u5339\u914D\u5E76\u81EA\u52A8\u6CE8\u518C ref,\u4E00\u6B21\u8C03\u7528\u66FF\u4EE3 \u5FEB\u7167\u2192\u67E5\u627E\u2192\u70B9\u51FB \u4E09\u8F6E;\u96F6\u547D\u4E2D\u62A5\u9519\u4F1A\u9644\u6700\u76F8\u8FD1\u5019\u9009\u3002\u9700\u8981\u7CBE\u786E\u63A7\u5236\u65F6\u518D\u7528 selector(CSS)/ref(aria \u5FEB\u7167)\u3002\u6253\u5F00\u9875\u9762\u540E\u53EF\u5148 browser_snapshot \u8BFB\u6587\u672C\u4E0E\u94FE\u63A5\u3001browser_elements \u8BFB\u8868\u5355\u7ED3\u6784(\u542B\u5173\u8054 label \u4E0E\u5C55\u5F00\u7684\u4E0B\u62C9\u9009\u9879);browser_find(query \u4F20\u540D\u79F0,\u53EA\u8FD4\u56DE\u547D\u4E2D ref+\u533A\u57DF\u4E0A\u4E0B\u6587,\u5927\u9875\u9762\u6700\u7701 token;\u96F6\u547D\u4E2D\u4F1A\u7ED9\u76F8\u8FD1\u5019\u9009)\u9002\u5408\u591A\u76EE\u6807\u6392\u67E5,\u9700\u8981\u5168\u9875\u7ED3\u6784\u65F6\u518D\u7528 browser_snapshot { mode: \"aria\" }(\u53EF\u4EA4\u4E92\u5143\u7D20\u5E26 ref=eN,click/type/select/press \u76F4\u63A5\u4F20 ref \u4EE3\u66FF CSS \u9009\u62E9\u5668);\u4E0B\u62C9\u9009\u62E9(\u539F\u751F select \u4E0E antd/element \u7EC4\u4EF6\u5E93)\u7528 browser_select \u4F20 selector/ref/what + option \u6587\u672C\u4E00\u6B65\u5B8C\u6210;\u56FA\u5B9A\u5C0F\u5E8F\u5217(\u70B9\u51FB\u2192\u7B49\u5F85\u2192\u8F93\u5165\u2192\u63D0\u4EA4)\u7528 browser_batch \u4E00\u6B21\u6267\u884C\u51CF\u5C11\u5F80\u8FD4;\u6807\u7B7E\u7BA1\u7406\u7528 browser_tabs;\u4E0B\u8F7D\u7528 browser_download(wait=true \u53EF\u7B49\u65B0\u4E0B\u8F7D);\u767B\u5F55\u6001\u7528 browser_cookies\u3002\u505A\u81EA\u52A8\u5316\u6D4B\u8BD5\u65F6:\u7528 browser_wait_for \u663E\u5F0F\u7B49\u5F85\u9875\u9762\u7A33\u5B9A/\u5143\u7D20\u51FA\u73B0,\u7528 browser_assert \u65AD\u8A00(\u5931\u8D25\u81EA\u52A8\u622A\u56FE;visual={baseline} \u53EF\u505A\u89C6\u89C9\u56DE\u5F52,\u5148\u7528 browser_screenshot { baseline } \u5EFA\u57FA\u7EBF),\u7528 browser_network \u68C0\u67E5\u63A5\u53E3\u8C03\u7528\u4E0E 4xx/5xx,\u8DD1\u5B8C\u4E00\u8F6E\u53EF\u7528 browser_record save \u4FDD\u5B58\u64CD\u4F5C\u5E8F\u5217\u3001browser_replay \u56DE\u653E\u505A\u56DE\u5F52(\u9009\u62E9\u5668\u5931\u6548\u81EA\u52A8\u6309\u6307\u7EB9\u81EA\u6108;vars/data \u652F\u6301\u53C2\u6570\u5316\u4E0E\u6570\u636E\u9A71\u52A8)\u3002\u6BCF\u6B21 replay/batch \u90FD\u8BB0\u5165\u8FD0\u884C\u5386\u53F2,browser_report list/export \u53EF\u67E5\u770B\u4E0E\u5BFC\u51FA\u56DE\u5F52\u62A5\u544A\u3002\u6293\u53D6\u7ED3\u6784\u5316\u6570\u636E\u7528 browser_extract(fields \u5B9A\u4E49 CSS \u5B57\u6BB5,\u53EF\u7FFB\u9875,\u53EF\u5BFC\u51FA CSV/JSONL)\u3002\u9047\u5230\u9700\u8981\u767B\u5F55\u7684\u7F51\u7AD9:\u5148 browser_open \u6253\u5F00\u767B\u5F55\u9875,\u544A\u77E5\u7528\u6237\
|
|
3
|
-
declare const CORE_TOOL_PROMPT = "\u6D4F\u89C8\u5668\u5DE5\u5177\u64CD\u4F5C\u4E00\u4E2A\u5171\u4EAB\u6D4F\u89C8\u5668\u4F1A\u8BDD(toolset=core:\u5DF2\u7CBE\u7B80\u5DE5\u5177\u96C6)\u3002\u5B9A\u4F4D\u64CD\u4F5C\u5143\u7D20\u6700\u7701\u4E8B\u7684\u65B9\u5F0F\u662F\u610F\u56FE\u8BED\u4E49\u5B9A\u4F4D:browser_click/browser_type/browser_select \u76F4\u63A5\u4F20 what(\u5143\u7D20\u540D\u79F0/label,\u5982 what:'\u767B\u5F55'),\u4E00\u6B21\u8C03\u7528\u66FF\u4EE3\u5FEB\u7167\u2192\u67E5\u627E\u2192\u70B9\u51FB;\u96F6\u547D\u4E2D\u62A5\u9519\u9644\u6700\u76F8\u8FD1\u5019\u9009\u3002\u9700\u8981\u7CBE\u786E\u63A7\u5236\u65F6\u518D\u7528 selector/ref:\u6253\u5F00\u9875\u9762\u540E\u7528 browser_snapshot \u8BFB\u5185\u5BB9(scope \u9650\u5B9A\u533A\u57DF\u3001since \u589E\u91CF\u91CD\u8BFB\u7701 token),browser_find(query \u4F20\u540D\u79F0,\u8FD4\u56DE ref),\u70B9\u51FB/\u8F93\u5165/\u6309\u952E/\u4E0B\u62C9\u7528 browser_click/browser_type/browser_press/browser_select(selector \u6216 ref);\u56FA\u5B9A\u5C0F\u5E8F\u5217\u7528 browser_batch;\u7B49\u5F85\u7528 browser_wait/browser_wait_for,\u65AD\u8A00\u7528 browser_assert;\u63A5\u53E3\u68C0\u67E5\u7528 browser_network;\u6807\u7B7E browser_tabs;\u4E0B\u8F7D/\u4E0A\u4F20 browser_download/browser_upload\u3002\u9700\u8981\u4EBA\u5DE5:\u767B\u5F55\u7528 browser_wait_for_login,\u9A8C\u8BC1\u7801/\u4E8C\u6B21\u786E\u8BA4\u7528 browser_pause_for_human,\u654F\u611F\u52A8\u4F5C(\u63D0\u4EA4/\u5220\u9664/\u652F\u4ED8)\u524D\u7528 browser_confirm\u3002\u73AF\u5883\u5F02\u5E38\u5148\u8DD1 browser_doctor \u8BCA\u65AD\u3002\u5F55\u5236/\u56DE\u653E/\u62A5\u544A/\u62BD\u53D6/profile/\u9694\u79BB context \u7B49\u5DE5\u5177\u5F53\u524D\u672A\u6CE8\u518C(toolset=core),\u9700\u8981\u65F6\u628A\u914D\u7F6E\u6539\u4E3A toolset: all \u5E76\u91CD\u542F DSH\u3002\u7528\u6237\u53EF\u901A\u8FC7\u754C\u9762\u53F3\u4E0B\u89D2\u9762\u677F\u7684\u5B9E\u65F6\u753B\u9762\u67E5\u770B\u6D4F\u89C8\u5668\u3002";
|
|
2
|
+
declare const FULL_TOOL_PROMPT = "\u6D4F\u89C8\u5668\u5DE5\u5177\u64CD\u4F5C\u4E00\u4E2A\u5171\u4EAB\u6D4F\u89C8\u5668\u4F1A\u8BDD,\u652F\u6301\u591A\u6807\u7B7E\u9875\u4E0E\u591A\u914D\u7F6E\u6587\u4EF6\u3002\u5B9A\u4F4D\u64CD\u4F5C\u5143\u7D20\u6700\u7701\u4E8B\u7684\u65B9\u5F0F\u662F\u610F\u56FE\u8BED\u4E49\u5B9A\u4F4D:browser_click/browser_type/browser_select \u76F4\u63A5\u4F20 what(\u5143\u7D20\u540D\u79F0/label,\u5982 what:'\u767B\u5F55'),\u9875\u5185\u6253\u5206\u5339\u914D\u5E76\u81EA\u52A8\u6CE8\u518C ref,\u4E00\u6B21\u8C03\u7528\u66FF\u4EE3 \u5FEB\u7167\u2192\u67E5\u627E\u2192\u70B9\u51FB \u4E09\u8F6E;\u96F6\u547D\u4E2D\u62A5\u9519\u4F1A\u9644\u6700\u76F8\u8FD1\u5019\u9009\u3002\u9700\u8981\u7CBE\u786E\u63A7\u5236\u65F6\u518D\u7528 selector(CSS)/ref(aria \u5FEB\u7167)\u3002\u6253\u5F00\u9875\u9762\u540E\u53EF\u5148 browser_snapshot \u8BFB\u6587\u672C\u4E0E\u94FE\u63A5\u3001browser_elements \u8BFB\u8868\u5355\u7ED3\u6784(\u542B\u5173\u8054 label \u4E0E\u5C55\u5F00\u7684\u4E0B\u62C9\u9009\u9879);browser_find(query \u4F20\u540D\u79F0,\u53EA\u8FD4\u56DE\u547D\u4E2D ref+\u533A\u57DF\u4E0A\u4E0B\u6587,\u5927\u9875\u9762\u6700\u7701 token;\u96F6\u547D\u4E2D\u4F1A\u7ED9\u76F8\u8FD1\u5019\u9009)\u9002\u5408\u591A\u76EE\u6807\u6392\u67E5,\u9700\u8981\u5168\u9875\u7ED3\u6784\u65F6\u518D\u7528 browser_snapshot { mode: \"aria\" }(\u53EF\u4EA4\u4E92\u5143\u7D20\u5E26 ref=eN,click/type/select/press \u76F4\u63A5\u4F20 ref \u4EE3\u66FF CSS \u9009\u62E9\u5668);\u4E0B\u62C9\u9009\u62E9(\u539F\u751F select \u4E0E antd/element \u7EC4\u4EF6\u5E93)\u7528 browser_select \u4F20 selector/ref/what + option \u6587\u672C\u4E00\u6B65\u5B8C\u6210;\u56FA\u5B9A\u5C0F\u5E8F\u5217(\u70B9\u51FB\u2192\u7B49\u5F85\u2192\u8F93\u5165\u2192\u63D0\u4EA4)\u7528 browser_batch \u4E00\u6B21\u6267\u884C\u51CF\u5C11\u5F80\u8FD4;\u6807\u7B7E\u7BA1\u7406\u7528 browser_tabs;\u4E0B\u8F7D\u7528 browser_download(wait=true \u53EF\u7B49\u65B0\u4E0B\u8F7D);\u767B\u5F55\u6001\u7528 browser_cookies\u3002\u505A\u81EA\u52A8\u5316\u6D4B\u8BD5\u65F6:\u7528 browser_wait_for \u663E\u5F0F\u7B49\u5F85\u9875\u9762\u7A33\u5B9A/\u5143\u7D20\u51FA\u73B0,\u7528 browser_assert \u65AD\u8A00(\u5931\u8D25\u81EA\u52A8\u622A\u56FE;visual={baseline} \u53EF\u505A\u89C6\u89C9\u56DE\u5F52,\u5148\u7528 browser_screenshot { baseline } \u5EFA\u57FA\u7EBF),\u7528 browser_network \u68C0\u67E5\u63A5\u53E3\u8C03\u7528\u4E0E 4xx/5xx,\u8DD1\u5B8C\u4E00\u8F6E\u53EF\u7528 browser_record save \u4FDD\u5B58\u64CD\u4F5C\u5E8F\u5217\u3001browser_replay \u56DE\u653E\u505A\u56DE\u5F52(\u9009\u62E9\u5668\u5931\u6548\u81EA\u52A8\u6309\u6307\u7EB9\u81EA\u6108;vars/data \u652F\u6301\u53C2\u6570\u5316\u4E0E\u6570\u636E\u9A71\u52A8)\u3002\u6BCF\u6B21 replay/batch \u90FD\u8BB0\u5165\u8FD0\u884C\u5386\u53F2,browser_report list/export \u53EF\u67E5\u770B\u4E0E\u5BFC\u51FA\u56DE\u5F52\u62A5\u544A\u3002\u6293\u53D6\u7ED3\u6784\u5316\u6570\u636E\u7528 browser_extract(fields \u5B9A\u4E49 CSS \u5B57\u6BB5,\u53EF\u7FFB\u9875,\u53EF\u5BFC\u51FA CSV/JSONL)\u3002\u9047\u5230\u9700\u8981\u767B\u5F55\u7684\u7F51\u7AD9:\u5148 browser_open \u6253\u5F00\u767B\u5F55\u9875,\u544A\u77E5\u7528\u6237\u5728\u6D4F\u89C8\u5668\u7A97\u53E3\u5B8C\u6210\u767B\u5F55\u540E\u70B9\u9762\u677F\u767B\u5F55\u5361\u4E0A\u7684\u300C\u6211\u5DF2\u5B8C\u6210\u767B\u5F55\u300D,\u518D\u8C03\u7528 browser_wait_for_login \u7B49\u5F85(\u7528\u6237\u70B9\u6309\u94AE\u5373\u8FD4\u56DE,\u4E0D\u81EA\u52A8\u731C\u6D4B\u767B\u5F55\u72B6\u6001;\u4E5F\u53EF\u7ED9 successSelector/successUrl \u4F5C\u786E\u5B9A\u6027\u81EA\u52A8\u6761\u4EF6;\u65E0\u9762\u677F\u73AF\u5883\u5FC5\u987B\u7ED9\u5176\u4E00,\u5426\u5219\u8C03\u7528\u4F1A\u62A5\u9519);\u9A8C\u8BC1\u7801/\u4E8C\u6B21\u786E\u8BA4\u7B49\u66F4\u5E7F\u7684\u300C\u4EBA\u8865\u4E00\u6B65\u300D\u573A\u666F\u7528 browser_pause_for_human(\u63D0\u793A\u8BED\u4F1A\u663E\u793A\u5728\u5B9E\u65F6\u753B\u9762\u9762\u677F);\u914D\u7F6E userDataDir \u65F6\u767B\u5F55\u6001\u4F1A\u6301\u4E45\u5316\u3002\u7528\u6237\u53EF\u901A\u8FC7\u754C\u9762\u53F3\u4E0B\u89D2\u9762\u677F\u7684\u5B9E\u65F6\u753B\u9762\u67E5\u770B\u6D4F\u89C8\u5668\u3002";
|
|
3
|
+
declare const CORE_TOOL_PROMPT = "\u6D4F\u89C8\u5668\u5DE5\u5177\u64CD\u4F5C\u4E00\u4E2A\u5171\u4EAB\u6D4F\u89C8\u5668\u4F1A\u8BDD(toolset=core:\u5DF2\u7CBE\u7B80\u5DE5\u5177\u96C6)\u3002\u5B9A\u4F4D\u64CD\u4F5C\u5143\u7D20\u6700\u7701\u4E8B\u7684\u65B9\u5F0F\u662F\u610F\u56FE\u8BED\u4E49\u5B9A\u4F4D:browser_click/browser_type/browser_select \u76F4\u63A5\u4F20 what(\u5143\u7D20\u540D\u79F0/label,\u5982 what:'\u767B\u5F55'),\u4E00\u6B21\u8C03\u7528\u66FF\u4EE3\u5FEB\u7167\u2192\u67E5\u627E\u2192\u70B9\u51FB;\u96F6\u547D\u4E2D\u62A5\u9519\u9644\u6700\u76F8\u8FD1\u5019\u9009\u3002\u9700\u8981\u7CBE\u786E\u63A7\u5236\u65F6\u518D\u7528 selector/ref:\u6253\u5F00\u9875\u9762\u540E\u7528 browser_snapshot \u8BFB\u5185\u5BB9(scope \u9650\u5B9A\u533A\u57DF\u3001since \u589E\u91CF\u91CD\u8BFB\u7701 token),browser_find(query \u4F20\u540D\u79F0,\u8FD4\u56DE ref),\u70B9\u51FB/\u8F93\u5165/\u6309\u952E/\u4E0B\u62C9\u7528 browser_click/browser_type/browser_press/browser_select(selector \u6216 ref);\u56FA\u5B9A\u5C0F\u5E8F\u5217\u7528 browser_batch;\u7B49\u5F85\u7528 browser_wait/browser_wait_for,\u65AD\u8A00\u7528 browser_assert;\u63A5\u53E3\u68C0\u67E5\u7528 browser_network;\u6807\u7B7E browser_tabs;\u4E0B\u8F7D/\u4E0A\u4F20 browser_download/browser_upload\u3002\u9700\u8981\u4EBA\u5DE5:\u767B\u5F55\u7528 browser_wait_for_login(\u7528\u6237\u5728\u9762\u677F\u70B9\u300C\u6211\u5DF2\u5B8C\u6210\u767B\u5F55\u300D\u5373\u8FD4\u56DE;\u65E0\u9762\u677F\u73AF\u5883\u5FC5\u987B\u7ED9 successSelector/successUrl,\u5426\u5219\u8C03\u7528\u62A5\u9519),\u9A8C\u8BC1\u7801/\u4E8C\u6B21\u786E\u8BA4\u7528 browser_pause_for_human,\u654F\u611F\u52A8\u4F5C(\u63D0\u4EA4/\u5220\u9664/\u652F\u4ED8)\u524D\u7528 browser_confirm\u3002\u73AF\u5883\u5F02\u5E38\u5148\u8DD1 browser_doctor \u8BCA\u65AD\u3002\u5F55\u5236/\u56DE\u653E/\u62A5\u544A/\u62BD\u53D6/profile/\u9694\u79BB context \u7B49\u5DE5\u5177\u5F53\u524D\u672A\u6CE8\u518C(toolset=core),\u9700\u8981\u65F6\u628A\u914D\u7F6E\u6539\u4E3A toolset: all \u5E76\u91CD\u542F DSH\u3002\u7528\u6237\u53EF\u901A\u8FC7\u754C\u9762\u53F3\u4E0B\u89D2\u9762\u677F\u7684\u5B9E\u65F6\u753B\u9762\u67E5\u770B\u6D4F\u89C8\u5668\u3002";
|
|
4
4
|
/** 组装完整的工具使用指引文本(DSH systemPrompt 段落与 MCP instructions 共用)。 */
|
|
5
5
|
export declare function browserSystemPrompt(config: Pick<BrowserConfig, "toolset" | "confirmMode">): string;
|
|
6
6
|
export { FULL_TOOL_PROMPT, CORE_TOOL_PROMPT };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-view",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "DSH 插件:共享 Playwright 浏览器自动化(38 个 browser_* 工具,含 what 意图语义定位(一次调用替代快照→查找→点击)、SoM 标徽看图点击与零命中截图兜底、SPA ref 软失效重锚定、open Shadow DOM 组合树穿透、跨 Frame/IFrame 定位、视口感知动态快照、批量表单智能填充、点击效果验证与定位失败遥测、Playwright 规范代码导出、批处理、视觉回归、选择器自愈、增量快照)+ 人机协作(面板点选教学 browser_pick_element、人工接管等待、敏感操作审批硬拦截、人机混合录制、面板一键重跑)+ CDP 流式推流、拟人化轨迹与高斯击键、全景时光机、企业富控件矩阵。另附独立 MCP server 入口(bin dsh-browser-mcp,stdio),任意 MCP 客户端可直接调用全部工具。运行时依赖 playwright-core 与 @modelcontextprotocol/sdk(仅 MCP 入口,不进 DSH 插件加载链)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|