dsh-browser-view 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -15
- package/lib/client.js +1 -1
- package/lib/index.js +1913 -105
- package/lib/types/browser/extract.d.ts +13 -0
- package/lib/types/browser/recordings.d.ts +4 -9
- package/lib/types/browser/reports.d.ts +42 -0
- package/lib/types/browser/session.d.ts +16 -7
- package/lib/types/browser/tools.d.ts +222 -24
- package/lib/types/browser/types.d.ts +36 -0
- package/lib/types/client/index.d.ts +17 -7
- package/lib/types/lib/tools.d.ts +11 -0
- package/lib/types/lib/types.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# dsh-browser-view
|
|
2
2
|
|
|
3
|
-
DSH 插件:共享 Playwright 浏览器自动化 +
|
|
3
|
+
DSH 插件:共享 Playwright 浏览器自动化 + 原生界面实时画面面板。一次安装,三件事:
|
|
4
4
|
|
|
5
5
|
| 模块 | 做什么 |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| 🧭 **浏览器自动化** | 共享 Playwright 浏览器:多标签、多 profile、下载/上传、Cookie、表单回放,
|
|
7
|
+
| 🧭 **浏览器自动化** | 共享 Playwright 浏览器:多标签、多 profile、下载/上传、Cookie、表单回放,31 个 `browser_*` 工具 |
|
|
8
8
|
| 🖥️ **实时画面** | 注入 DSH 界面右下角悬浮面板:浏览器状态 + 实时缩略图,点开大屏模态框(2 秒轮询画面 + 访问历史) |
|
|
9
|
+
| ✅ **回归测试闭环** | 录制回放 + 选择器自愈 + 参数化/数据驱动 + 视觉回归 + 运行历史与报告导出 |
|
|
9
10
|
|
|
10
11
|
设计原则:能做的一次性操作交给 agent 用 bash 完成,插件只做 bash 做不到的常驻浏览器自动化。唯一第三方运行时依赖是 `playwright-core`(复用系统已装的 Edge/Chrome,无需下载 Chromium),不依赖任何 `@deepseek-ai` 运行时包。
|
|
11
12
|
|
|
@@ -29,6 +30,7 @@ dsh plugin --profile web add dsh-browser-view
|
|
|
29
30
|
headless: false # 默认有头(本机调试/人工登录);服务器场景可改 true
|
|
30
31
|
userDataDir: "" # 设置后登录态跨 DSH 重启保留
|
|
31
32
|
profiles: {} # { work: { userDataDir, channel, headless }, ... }
|
|
33
|
+
workspaceDir: "" # 截图/下载/上传的落盘基准目录(默认探测会话 cwd,多工作区/服务器场景可钉死)
|
|
32
34
|
screenshotDir: .dsh-browser/screenshots
|
|
33
35
|
downloadDir: .dsh-browser/downloads
|
|
34
36
|
screenshotMaxAgeDays: 7 # 截图保留天数(0 = 不按时间清理)
|
|
@@ -37,6 +39,11 @@ dsh plugin --profile web add dsh-browser-view
|
|
|
37
39
|
maxTextChars: 20000
|
|
38
40
|
maxLinks: 50
|
|
39
41
|
timeoutMs: 30000
|
|
42
|
+
baselineDir: .dsh-browser/baselines # 视觉回归基线目录(不受截图清理约束)
|
|
43
|
+
selectorHealing: true # 回放/批处理选择器自愈(默认开;只作用于失败路径)
|
|
44
|
+
healTimeoutMs: 3000 # 自愈时单次尝试的超时
|
|
45
|
+
maxRefs: 200 # ARIA 快照每次分配的 ref 上限
|
|
46
|
+
maxRows: 1000 # browser_extract 单次聚合行数上限
|
|
40
47
|
```
|
|
41
48
|
|
|
42
49
|
## 浏览器工具
|
|
@@ -45,25 +52,29 @@ dsh plugin --profile web add dsh-browser-view
|
|
|
45
52
|
| --- | --- |
|
|
46
53
|
| `browser_open` | 打开 URL |
|
|
47
54
|
| `browser_tabs` | 标签管理:list / new / switch / close(按 id 或 index) |
|
|
48
|
-
| `browser_snapshot` |
|
|
49
|
-
| `browser_elements` | 可交互元素结构化清单(input/button/select/链接 + 现成 CSS
|
|
50
|
-
| `browser_click` / `browser_type` / `browser_press` | 点击 / 输入(可清空、可回车提交)/
|
|
55
|
+
| `browser_snapshot` | 页面快照:`mode: "text"`(默认)返回压缩后的文本/链接/输入框;`mode: "aria"` 返回可访问性树,可交互元素带 `[ref=eN]` 短引用 |
|
|
56
|
+
| `browser_elements` | 可交互元素结构化清单(input/button/select/链接 + 关联 label + 现成 CSS 选择器;有展开下拉时附可见选项) |
|
|
57
|
+
| `browser_click` / `browser_type` / `browser_press` | 点击 / 输入(可清空、可回车提交)/ 按键;`selector`(CSS)与 `ref`(aria 快照)二选一 |
|
|
58
|
+
| `browser_select` | 下拉选择原语:原生 `<select>` 与 antd / element-plus / ARIA listbox 通用,selector/ref + option 文本一步完成;找不到选项时报错并列出可见选项 |
|
|
51
59
|
| `browser_eval` | 在页面上下文执行 JS(结果自动净化成 lossless JSON) |
|
|
52
|
-
| `
|
|
60
|
+
| `browser_batch` | 动作批处理:一次调用顺序执行多个步骤 `[{tool, args}]`(点击→等待→输入→提交类小序列减少往返;禁止嵌套;可录制可回放) |
|
|
61
|
+
| `browser_screenshot` | 截图存为 PNG(自动清理,见下);`baseline: "<名>"` 存为视觉回归基线(不受清理约束) |
|
|
53
62
|
| `browser_wait` / `browser_back` / `browser_reload` | 等待 / 后退 / 刷新 |
|
|
54
63
|
| `browser_wait_for_login` | 等待人工登录完成(配 `headless: false` 使用,见下) |
|
|
55
64
|
| `browser_wait_for` | 通用显式等待(元素状态 / URL / 文本 / 数量 / eval,超时抛错) |
|
|
56
|
-
| `browser_assert` | 结构化断言(失败自动截图存证据目录并报错) |
|
|
57
|
-
| `browser_network` | 接口记录查询:list / failed(4xx/5xx)/ wait / clear,含请求/响应体(截断) |
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
65
|
+
| `browser_assert` | 结构化断言(失败自动截图存证据目录并报错);`visual: { baseline }` 做视觉回归断言(见下) |
|
|
66
|
+
| `browser_network` | 接口记录查询:list / failed(4xx/5xx)/ wait(命中返回 waitMs,超时附最近请求摘要)/ stats(按 host 聚合计数/失败数/均耗时/状态码分布)/ clear,含请求/响应体(截断) |
|
|
67
|
+
| `browser_extract` | 规则化结构化抽取:fields 定义 CSS 字段(支持嵌套/multiple),`next` 自动翻页聚合,导出 json / csv / jsonl / markdown 落盘工作区 |
|
|
68
|
+
| `browser_record` | 操作录制:start / stop / save(list/delete 管理;save 支持 `stripReads` 剔除只读步骤、`vars` 把指定步骤字段抽成 `{{占位符}}`;selector 步骤自动附带元素指纹供回放自愈) |
|
|
69
|
+
| `browser_replay` | 回放录制或步骤序列(回归测试,遇错可停);`vars` 注入占位符,`data` / `dataFile`(CSV)逐行数据驱动;选择器失效自动按指纹自愈 |
|
|
70
|
+
| `browser_report` | 回归报告:list / show / export(运行历史自动记录,export 聚合为 Markdown / 自包含 HTML 落盘) |
|
|
60
71
|
| `browser_status` / `browser_close` | 状态 / 关闭会话 |
|
|
61
|
-
| `browser_download` / `browser_upload` | 下载(
|
|
72
|
+
| `browser_download` / `browser_upload` | 下载(保存到工作区;`wait: true` 可轮询等待新下载)/ 上传本地文件 |
|
|
62
73
|
| `browser_cookies` | Cookie 管理(list / set / clear,登录态处理) |
|
|
63
74
|
| `browser_form` / `browser_form_save` / `browser_forms` | 批量填表 / 保存回放 / 管理已存表单 |
|
|
64
75
|
| `browser_profile` | 多配置文件:work/personal 等独立会话与登录态(userDataDir 持久化) |
|
|
65
76
|
|
|
66
|
-
**截图自动清理**:`browser_screenshot` 保存的 `.png` 自动修剪——每次截图后即时清理 + 每小时定时清扫;默认保留最近 7 天、最多 200 张(`screenshotMaxAgeDays` / `screenshotMaxCount`,设为 0 关闭对应规则)
|
|
77
|
+
**截图自动清理**:`browser_screenshot` 保存的 `.png` 自动修剪——每次截图后即时清理 + 每小时定时清扫;默认保留最近 7 天、最多 200 张(`screenshotMaxAgeDays` / `screenshotMaxCount`,设为 0 关闭对应规则),只清理截图目录直属文件。视觉回归基线(`baselineDir`)不参与清理。
|
|
67
78
|
|
|
68
79
|
> 首次调用浏览器工具时自动启动浏览器;启动失败会提示安装 Chromium(`npx playwright install chromium`)或配置 `executablePath`。
|
|
69
80
|
|
|
@@ -84,11 +95,12 @@ browser_screenshot # 证据
|
|
|
84
95
|
|
|
85
96
|
**接口出入参**:`browser_network` 自动记录每个 XHR/fetch 的请求体(`postData`)与响应体(`body`,均截断:请求体 2000 字符、响应体 4000 字符;SSE 流与 >500KB 响应跳过;`config.recordBodies: false` 可关闭)。`wait` 匹配到的记录同样带出入参,可直接断言接口返回结构。
|
|
86
97
|
|
|
87
|
-
**条件格式**(`browser_wait_for` / `browser_assert`
|
|
98
|
+
**条件格式**(`browser_wait_for` / `browser_assert` 共用):
|
|
88
99
|
|
|
89
100
|
| 字段 | 示例 | 说明 |
|
|
90
101
|
| --- | --- | --- |
|
|
91
102
|
| `selector` | `{ selector: "#btn", state: "visible" }` | 元素状态:visible / hidden / attached / detached |
|
|
103
|
+
| `ref` | `{ ref: "e3", state: "visible" }` | aria 快照 ref 的状态等待 |
|
|
92
104
|
| `url` | `{ url: "https://.*\\.example\\.com" }` | 当前 URL 正则匹配 |
|
|
93
105
|
| `text` | `{ text: "保存成功" }` | 页面可见文本包含 |
|
|
94
106
|
| `count` | `{ count: { selector: ".row", op: "gte", value: 10 } }` | 元素数量比较(eq/gt/gte/lt/lte) |
|
|
@@ -96,7 +108,88 @@ browser_screenshot # 证据
|
|
|
96
108
|
|
|
97
109
|
`browser_assert` 失败时自动截图(`assert-fail-*.png`,存截图目录,受清理策略约束)并抛错,错误消息含原因与截图路径。
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
**ARIA 快照 + ref 引用(推荐的定位方式)**:`browser_snapshot { mode: "aria" }` 返回页面的可访问性树,可交互元素带 `ref=eN` 短引用:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
- navigation "主导航"
|
|
115
|
+
- link "首页" [ref=e1]
|
|
116
|
+
- main
|
|
117
|
+
- textbox "用户名" [ref=e2]
|
|
118
|
+
- button "登录" [ref=e3]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
后续 `browser_click { ref: "e3" }`、`browser_type { ref: "e2", text }`、`browser_select { ref, option }`、`browser_press { ref, key }` 直接用 ref 代替 CSS 选择器——比手写选择器更稳、比文本快照更省 token。ref 解析时会校验元素的 role/name 仍一致(防漂移误点),不一致自动降级 `getByRole`;页面导航或重新快照后旧 ref 失效(报错提示重取)。读正文仍用默认 `mode: "text"`。
|
|
122
|
+
|
|
123
|
+
**动作批处理**:固定小序列用 `browser_batch` 一次往返执行:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
browser_batch { steps: [
|
|
127
|
+
{ tool: "browser_type", args: { ref: "e2", text: "user", clear: true } },
|
|
128
|
+
{ tool: "browser_type", args: { ref: "e3", text: "pass", clear: true } },
|
|
129
|
+
{ tool: "browser_click", args: { ref: "e4" } },
|
|
130
|
+
{ tool: "browser_wait_for", args: { condition: { text: "欢迎" } } },
|
|
131
|
+
] }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
步骤内禁止嵌套 batch/replay/record;批整体在操作轨迹与录制里记为单条步骤。
|
|
135
|
+
|
|
136
|
+
**回归测试**:agent 跑通一轮流程后 `browser_record save <name>`,以后用 `browser_replay { name }` 一键回放(默认遇错即停,`failFast: false` 可继续);也可直接传 `steps` 数组。**save 的数据源**:做过 `browser_record start` → `stop` 的,保存该段录制;从未录制的,保存最近 100 次工具调用(返回值的 `source` 字段会标明 `recording` / `recent-log`)。save 时会检查 `browser_upload` 步骤引用的本地文件,缺失的以 `warnings` 返回(回放将失败,请及时重建或改用稳定路径)。已保存录制写入 `$DSH_HOME/.dsh-browser/recordings/<name>.json`(原子替换,名字自动安全化防路径穿越),重启 DSH 后自动加载,可跨会话复用;`browser_record list / delete` 管理。**录制管理界面**:DSH 设置弹窗 → 左侧导航「浏览器」页,可查看列表、展开步骤详情、删除录制。所有工具调用自动记录操作轨迹(面板显示最近 8 条,`/browser/log` 可取全量)。
|
|
137
|
+
|
|
138
|
+
**选择器自愈**:录制(save)时每个 selector 步骤自动附带元素指纹(`data-testid` / role+name / 可见文本 / id / 标签)。回放时原选择器失败会按 `testid → role+name → text → id → tag+text` 降级重试(每次尝试限时 `healTimeoutMs`,默认 3s,避免等满全局超时),自愈成功的步骤在结果中标记 `healed: true, via: "role+name", resolved: "<新选择器>"` 并计入 `healedCount`——提示你该修录制里的选择器了,而不是默默吞掉页面漂移。仅作用于 replay/batch 的失败路径,live 工具不受影响;`config.selectorHealing: false` 可整体关闭。录制格式为 formatVersion 2,旧格式录制正常回放(无自愈)。
|
|
139
|
+
|
|
140
|
+
**参数化与数据驱动**:save 时用 `vars` 把指定步骤的字段抽成占位符:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
browser_record { action: "save", name: "login",
|
|
144
|
+
vars: { username: "#1.args.text", password: "#2.args.text" } } # 步骤号 1 起
|
|
145
|
+
browser_replay { name: "login", vars: { username: "a@x.com", password: "***" } } # 单次注入
|
|
146
|
+
browser_replay { name: "login", data: [{ username: "a@x.com", password: "1" },
|
|
147
|
+
{ username: "b@x.com", password: "2" }] } # 逐行回放
|
|
148
|
+
browser_replay { name: "login", dataFile: "testdata/users.csv" } # CSV 首行为变量名
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
缺失的占位符直接报错(不做空串替换);data 模式每行独立 failFast、失败不中断后续行,返回 `runs` 按行聚合。注意 vars 值会随运行记录写入报告,敏感值请自行管理数据文件。
|
|
152
|
+
|
|
153
|
+
**视觉回归**:先用 `browser_screenshot { baseline: "login" }` 建立基线(存 `.dsh-browser/baselines/`,不受截图清理约束),之后在断言中比对:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
browser_assert { visual: { baseline: "login", threshold: 0.1, maxDiffRatio: 0.005,
|
|
157
|
+
mask: [".timestamp", ".ad-banner"] } }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- 逐像素加权亮度差超过 `threshold`(0–1,默认 0.1)计为差异;`diffRatio` 超过 `maxDiffRatio`(默认 0.5%)判失败;
|
|
161
|
+
- `mask` 为忽略区域选择器数组(时钟/广告位/随机头像等动态内容),两图同步填白后比对;
|
|
162
|
+
- 尺寸不一致直接失败;失败时落盘当前截图与红标 diff 图,路径在错误消息里;
|
|
163
|
+
- 超过 1000px 边长的大图会先等比缩放再比对(性能折中,亚像素级差异可能漏检);
|
|
164
|
+
- 零额外依赖:像素比对在浏览器内 canvas 完成。
|
|
165
|
+
|
|
166
|
+
**回归报告**:每次 `browser_replay` / `browser_batch` 自动记入运行历史(`$DSH_HOME/.dsh-browser/reports/runs.jsonl`,上限 1000 条):
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
browser_report { action: "list" } # 最近运行
|
|
170
|
+
browser_report { action: "show" } # 最近一条详情
|
|
171
|
+
browser_report { action: "export", format: "html", file: "reports/r.html" }
|
|
172
|
+
browser_report { action: "export", source: "login" } # 只聚合某录制
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
报告含运行趋势、失败步骤 Top、自愈步骤清单(建议修复录制选择器)。面板状态行会显示最近一次回归结果。**定时回归**:用 DSH 的 schedule 能力组合即可——定时触发 `browser_replay { name }` + `browser_report { action: "export" }`,早上看报告。
|
|
176
|
+
|
|
177
|
+
**结构化抽取**:`browser_extract` 做规则化抓取(CSS/属性,不含模型),支持翻页与导出:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
browser_extract {
|
|
181
|
+
fields: [
|
|
182
|
+
{ name: "title", selector: "h1" },
|
|
183
|
+
{ name: "rows", selector: "table tbody tr", multiple: true,
|
|
184
|
+
fields: [{ name: "name", selector: "td:nth-of-type(1)" },
|
|
185
|
+
{ name: "link", selector: "a", attr: "href" }] },
|
|
186
|
+
],
|
|
187
|
+
next: ".pagination .next", maxPages: 5,
|
|
188
|
+
format: "csv", file: "output/rows.csv",
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`attr` 缺省取可见文本,可指定 `href` / `src` 等属性;`next` 翻页直到消失/禁用或达 `maxPages`;路径限工作区内。语义化抽取(「总结要点」「按 schema 出 JSON」)不在此工具范围——用 `browser_snapshot` 读文本后交给 `jev_extract` 等模型侧工具,插件负责拿到干净的候选文本。
|
|
100
193
|
|
|
101
194
|
## 人工登录(验证码 / 扫码 / 双因素)
|
|
102
195
|
|
|
@@ -121,7 +214,8 @@ config:
|
|
|
121
214
|
安装后 DSH 界面右下角出现 🌐 悬浮按钮(跟随 DSH 亮/暗主题):
|
|
122
215
|
|
|
123
216
|
- **状态行**:浏览器开关、标签数、当前 URL;
|
|
124
|
-
- **实时缩略图**:浏览器打开时显示,点击弹出大屏模态框——2 秒轮询实时画面,下方列出**访问历史**(最近 50 条,点击可在新标签打开)
|
|
217
|
+
- **实时缩略图**:浏览器打开时显示,点击弹出大屏模态框——2 秒轮询实时画面,下方列出**访问历史**(最近 50 条,点击可在新标签打开);
|
|
218
|
+
- **最近回归**:面板底部一行显示最近一次 `browser_replay` / `browser_batch` 的结果(通过数 / 自愈数 / 时间)。
|
|
125
219
|
|
|
126
220
|
## 开发
|
|
127
221
|
|
package/lib/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},a={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function o(){let[e,t]=(0,r.useState)(()=>typeof document<`u`&&document.body?.getAttribute(`data-ds-dark-theme`)!==null);return(0,r.useEffect)(()=>{if(typeof document>`u`)return;let e=new MutationObserver(()=>t(document.body?.getAttribute(`data-ds-dark-theme`)!==null));return e.observe(document.body,{attributes:!0,attributeFilter:[`data-ds-dark-theme`]}),()=>e.disconnect()},[]),e}function s(e){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 c(e){let t=s(o()?a:i),[n,c]=(0,r.useState)(`/browser`),[l,u]=(0,r.useState)(null),[d,f]=(0,r.useState)(``),[p,m]=(0,r.useState)({});(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&c(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0;return u(null),f(``),fetch(`${n}/recordings`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&u(t.recordings??[])}).catch(t=>{e&&f(t instanceof Error?t.message:String(t))}),()=>{e=!1}},[n]);let h=e=>{m(t=>{let r=t[e];if(r!==void 0&&r!==`loading`){let n={...t};return delete n[e],n}return fetch(`${n}/recordings/detail?name=${encodeURIComponent(e)}`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>m(n=>({...n,[e]:t}))).catch(()=>m(t=>({...t,[e]:`failed`}))),{...t,[e]:`loading`}})},g=e=>{fetch(`${n}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:e})}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(()=>{u(t=>t===null?t:t.filter(t=>t.name!==e)),m(t=>{let n={...t};return delete n[e],n})}).catch(()=>{})};return(0,r.createElement)(`div`,{style:t.root},(0,r.createElement)(`div`,{style:t.heading},`浏览器录制(agent 用 browser_record save <name> 创建,存储于 $DSH_HOME/.dsh-browser/recordings)`),d?(0,r.createElement)(`div`,{style:t.empty},`加载失败: ${d}`):l===null?(0,r.createElement)(`div`,{style:t.empty},`加载中…`):l.length===0?(0,r.createElement)(`div`,{style:t.empty},`暂无录制。让 agent 跑完一轮流程后 browser_record save <name> 创建。`):l.map(e=>{let n=p[e.name];return(0,r.createElement)(`div`,{key:e.name,style:t.itemBox},(0,r.createElement)(`div`,{style:t.row},(0,r.createElement)(`span`,{style:t.label,title:`点击展开步骤`,onClick:()=>h(e.name)},e.name),(0,r.createElement)(`span`,{style:t.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleString()}`),(0,r.createElement)(`button`,{style:t.btn,onClick:()=>h(e.name)},n===void 0?`展开`:`收起`),(0,r.createElement)(`button`,{style:t.delBtn,onClick:()=>g(e.name)},`删除`)),n===void 0?null:n===`loading`?(0,r.createElement)(`div`,{style:t.empty},`加载中…`):(0,r.createElement)(`div`,{style:t.stepsBox},n===`failed`?(0,r.createElement)(`div`,{style:t.empty},`加载失败`):n.steps.length===0?(0,r.createElement)(`div`,{style:t.empty},`(空步骤)`):n.steps.map((n,i)=>(0,r.createElement)(`div`,{key:`${e.name}-${i}`,style:t.stepLine,title:`${n.tool} ${JSON.stringify(n.args??{})}`},(0,r.createElement)(`span`,{style:t.meta},`#${i}`),(0,r.createElement)(`span`,{style:t.toolName},n.tool),(0,r.createElement)(`span`,{style:t.stepArgs},JSON.stringify(n.args??{}))))))}),l!==null&&(0,r.createElement)(`div`,{style:t.heading},`提示:agent 在对话中用 browser_record save <name> 创建录制。`))}let l=[`slots`];function u(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`浏览器`},c))}return n.apply=u,n.inject=l,t.exports}});
|
|
1
|
+
window.__ModuleLoader__.load({id:`dsh-browser-view`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i={primary:`#1f2937`,secondary:`#4b5563`,tertiary:`#6b7280`,border:`#d8dde3`,hoverBg:`#f1f4f7`,danger:`#dc2626`},a={primary:`#f3f4f6`,secondary:`#cbd5e1`,tertiary:`#94a3b8`,border:`#3f4a58`,hoverBg:`#2a3240`,danger:`#f87171`};function o(){let[e,t]=(0,r.useState)(()=>typeof document<`u`&&document.body?.getAttribute(`data-ds-dark-theme`)!==null);return(0,r.useEffect)(()=>{if(typeof document>`u`)return;let e=new MutationObserver(()=>t(document.body?.getAttribute(`data-ds-dark-theme`)!==null));return e.observe(document.body,{attributes:!0,attributeFilter:[`data-ds-dark-theme`]}),()=>e.disconnect()},[]),e}function s(e){let t=new Date(e);return`${(`0`+t.getHours()).slice(-2)}:${(`0`+t.getMinutes()).slice(-2)}`}function c(e){return{row:{display:`flex`,alignItems:`center`,gap:`8px`,minWidth:0},label:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,fontSize:`13px`,color:e.primary,cursor:`pointer`},meta:{color:e.tertiary,fontSize:`12px`,whiteSpace:`nowrap`},btn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.secondary,font:`inherit`,fontSize:`12px`,cursor:`pointer`},delBtn:{height:`24px`,padding:`0 10px`,border:`1px solid ${e.border}`,borderRadius:`6px`,background:`transparent`,color:e.danger,font:`inherit`,fontSize:`12px`,cursor:`pointer`},itemBox:{display:`flex`,flexDirection:`column`,gap:`6px`,border:`1px solid ${e.border}`,borderRadius:`8px`,padding:`8px 10px`},stepsBox:{display:`flex`,flexDirection:`column`,gap:`2px`,borderTop:`1px solid ${e.border}`,paddingTop:`6px`},stepLine:{display:`flex`,gap:`8px`,fontSize:`13px`,lineHeight:`1.7`,color:e.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`},toolName:{color:e.primary,flex:`none`,fontWeight:500},stepArgs:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,color:e.secondary},empty:{color:e.tertiary,fontSize:`12px`,textAlign:`center`,padding:`12px 0`},root:{display:`flex`,flexDirection:`column`,gap:`8px`,maxWidth:`720px`},heading:{color:e.tertiary,fontSize:`12px`,lineHeight:`1.6`}}}function l(){let e=o(),t=e?a:i,[n,c]=(0,r.useState)(`/browser`),[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)(!1),[p,m]=(0,r.useState)(null),[h,g]=(0,r.useState)([]),[_,v]=(0,r.useState)([]),[y,b]=(0,r.useState)(Date.now()),[x,S]=(0,r.useState)(null);(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&c(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0,t=()=>{fetch(`${n}/status`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&(m(t),t?.open&&b(Date.now()))}).catch(()=>{e&&m(null)}),fetch(`${n}/log`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&t&&Array.isArray(t.calls)&&g(t.calls.slice(0,8))}).catch(()=>{}),fetch(`${n}/runs/latest`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&S(t?.run??null)}).catch(()=>{})};t();let r=setInterval(t,3e3);return()=>{e=!1,clearInterval(r)}},[n]),(0,r.useEffect)(()=>{if(!d)return;let e=!0,t=()=>{fetch(`${n}/history`,{cache:`no-store`}).then(e=>e.ok?e.json():null).then(t=>{e&&t&&Array.isArray(t.history)&&v(t.history)}).catch(()=>{}),b(Date.now())};t();let r=setInterval(t,2e3);return()=>{e=!1,clearInterval(r)}},[d,n]),(0,r.useEffect)(()=>{if(!d)return;let e=e=>{e.key===`Escape`&&f(!1)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[d]);let C=`${n}/screenshot?v=${y}`;return(0,r.createElement)(`div`,{id:`dsh-browser-client-overlay`,style:{position:`fixed`,inset:0,pointerEvents:`none`,zIndex:2147483e3}},(0,r.createElement)(`button`,{type:`button`,title:`浏览器自动化与实时画面`,onClick:()=>u(e=>!e),style:{position:`fixed`,right:`16px`,bottom:`16px`,width:`42px`,height:`42px`,borderRadius:`999px`,border:`1px solid ${t.border}`,background:e?`#1e2430`:`#ffffff`,color:t.primary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`18px`,boxShadow:`0 4px 16px rgba(0, 0, 0, 0.25)`,pointerEvents:`auto`,userSelect:`none`}},`🌐`),l&&(0,r.createElement)(`div`,{style:{position:`fixed`,right:`16px`,bottom:`66px`,width:`320px`,maxHeight:`72vh`,overflowY:`auto`,overflowX:`hidden`,display:`flex`,flexDirection:`column`,gap:`8px`,border:`1px solid ${t.border}`,borderRadius:`12px`,background:e?`#181e29`:`#ffffff`,boxShadow:`0 8px 32px rgba(0, 0, 0, 0.3)`,padding:`12px`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,padding:`4px 0`}},(0,r.createElement)(`span`,{style:{width:`8px`,height:`8px`,borderRadius:`999px`,flex:`none`,background:p?.open?`#10b981`:`#ef4444`}}),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,color:t.primary,fontSize:`13px`,fontWeight:600,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},`浏览器实时画面`),(0,r.createElement)(`span`,{style:{color:t.secondary,fontSize:`12px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,maxWidth:`160px`}},p===null?`未启用`:p.open?`${p.tabs??1} 标签 · ${(p.url||``).slice(0,20)}`:`未打开`)),p?.open?(0,r.createElement)(`img`,{src:C,alt:`实时画面`,title:`点击放大查看实时大屏与访问历史`,onClick:()=>f(!0),style:{width:`100%`,maxHeight:`160px`,objectFit:`cover`,borderRadius:`8px`,border:`1px solid ${t.border}`,cursor:`pointer`,background:e?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`11px`,lineHeight:`1.5`,padding:`8px`,background:e?`#121620`:`#f8fafc`,borderRadius:`6px`}},`浏览器尚未启动。在对话中让 Agent 调用 browser_open 即可呈现画面。`),h.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`3px`,borderTop:`1px solid ${t.border}`,paddingTop:`6px`,marginTop:`2px`}},(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`11px`,fontWeight:500,marginBottom:`2px`}},`最近操作 (${h.length} 条)`),h.map((e,n)=>(0,r.createElement)(`div`,{key:n,style:{display:`flex`,alignItems:`center`,gap:`6px`,fontSize:`12px`,color:t.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{flex:`none`,color:e.ok?`#10b981`:`#ef4444`,fontWeight:600}},e.ok?`✓`:`✗`),(0,r.createElement)(`span`,{style:{color:t.tertiary,fontSize:`11px`,flex:`none`}},s(e.ts)),(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`},title:`${e.tool}${e.error?`: ${e.error}`:``}`},`${e.tool}${e.error?`: ${e.error}`:``}`)))),x!==null&&(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,borderTop:`1px solid ${t.border}`,paddingTop:`6px`,marginTop:`2px`,fontSize:`12px`,color:t.secondary,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`},title:`runId=${x.runId} · browser_report 可导出报告`},(0,r.createElement)(`span`,{style:{flex:`none`,color:t.tertiary,fontSize:`11px`}},`最近回归`),(0,r.createElement)(`span`,{style:{flex:`none`,fontWeight:600,color:x.failed===0?`#10b981`:`#ef4444`}},x.failed===0?`✓`:`✗`),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},`${x.source}${x.row===void 0?``:` 行${x.row}`} ${x.passed}/${x.total}${x.healedCount>0?` · 自愈${x.healedCount}`:``} · ${new Date(x.ts).toLocaleTimeString(`zh-CN`,{hour12:!1})}`))),d&&(0,r.createElement)(`div`,{onClick:e=>{e.target===e.currentTarget&&f(!1)},style:{position:`fixed`,inset:0,zIndex:2147483001,display:`flex`,alignItems:`center`,justifyContent:`center`,padding:`24px`,background:`rgba(0, 0, 0, 0.65)`,pointerEvents:`auto`}},(0,r.createElement)(`div`,{style:{width:`min(1100px, 94vw)`,maxHeight:`90vh`,display:`flex`,flexDirection:`column`,gap:`10px`,border:`1px solid ${t.border}`,borderRadius:`12px`,background:e?`#181e29`:`#ffffff`,padding:`16px`,boxShadow:`0 16px 64px rgba(0, 0, 0, 0.4)`}},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`}},(0,r.createElement)(`span`,{style:{flex:1,minWidth:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`,color:t.primary,fontSize:`14px`,fontWeight:600}},p?.open?`${p.title||``} (${p.url||``})`:`浏览器实时大屏`),(0,r.createElement)(`button`,{type:`button`,onClick:()=>f(!1),style:{width:`28px`,height:`28px`,border:`1px solid ${t.border}`,borderRadius:`6px`,background:`transparent`,color:t.secondary,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`,fontSize:`14px`}},`✕`)),p?.open?(0,r.createElement)(`img`,{src:C,alt:`浏览器大图`,style:{width:`100%`,maxHeight:`55vh`,objectFit:`contain`,borderRadius:`8px`,border:`1px solid ${t.border}`,background:e?`#10141c`:`#f8fafc`}}):(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`13px`,textAlign:`center`,padding:`32px 0`}},`浏览器尚未打开`),_.length>0&&(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`4px`,maxHeight:`22vh`,overflowY:`auto`,borderTop:`1px solid ${t.border}`,paddingTop:`8px`}},(0,r.createElement)(`div`,{style:{color:t.tertiary,fontSize:`12px`,fontWeight:500}},`访问历史 (${_.length} 条,点击在新标签打开)`),_.map((e,n)=>(0,r.createElement)(`a`,{key:n,href:e.url,target:`_blank`,rel:`noopener noreferrer`,style:{display:`flex`,gap:`8px`,alignItems:`center`,color:t.secondary,fontSize:`12px`,textDecoration:`none`,borderRadius:`6px`,padding:`4px 6px`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`}},(0,r.createElement)(`span`,{style:{color:t.tertiary,flex:`none`}},s(e.ts)),(0,r.createElement)(`span`,{style:{overflow:`hidden`,textOverflow:`ellipsis`}},e.title||e.url)))))))}function u(e){let t=o(),n=t?a:i,s=c(n),[l,u]=(0,r.useState)(`/browser`),[d,f]=(0,r.useState)(null),[p,m]=(0,r.useState)(``),[h,g]=(0,r.useState)({});(0,r.useEffect)(()=>{let e=!0;return fetch(`/__dsh-browser-view__/config`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&typeof t.basePath==`string`&&t.basePath&&u(t.basePath.replace(/\/+$/,``))}).catch(()=>{}),()=>{e=!1}},[]),(0,r.useEffect)(()=>{let e=!0;return f(null),m(``),fetch(`${l}/recordings`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>{e&&f(t.recordings??[])}).catch(t=>{e&&m(t instanceof Error?t.message:String(t))}),()=>{e=!1}},[l]);let _=e=>{g(t=>{let n=t[e];if(n!==void 0&&n!==`loading`){let n={...t};return delete n[e],n}return fetch(`${l}/recordings/detail?name=${encodeURIComponent(e)}`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(t=>g(n=>({...n,[e]:t}))).catch(()=>g(t=>({...t,[e]:`failed`}))),{...t,[e]:`loading`}})},v=e=>{fetch(`${l}/recordings`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({name:e})}).then(e=>e.ok?e.json():Promise.reject(Error(`HTTP ${e.status}`))).then(()=>{f(t=>t===null?t:t.filter(t=>t.name!==e)),g(t=>{let n={...t};return delete n[e],n})}).catch(()=>{})};return(0,r.createElement)(`div`,{style:s.root},(0,r.createElement)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,paddingBottom:`12px`,marginBottom:`6px`,borderBottom:`1px solid ${n.border}`}},(0,r.createElement)(`div`,{style:{width:`38px`,height:`38px`,borderRadius:`10px`,display:`flex`,alignItems:`center`,justifyContent:`center`,background:t?`#222938`:`#eef2f6`,border:`1px solid ${n.border}`,fontSize:`20px`,flexShrink:0}},`🌐`),(0,r.createElement)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`2px`}},(0,r.createElement)(`div`,{style:{fontSize:`15px`,fontWeight:600,color:n.primary}},`浏览器自动化与录制`),(0,r.createElement)(`div`,{style:{fontSize:`12px`,color:n.tertiary}},`共享 Playwright 浏览器自动化、测试原语、操作轨迹与回归录制管理`))),(0,r.createElement)(`div`,{style:s.heading},`已保存的操作录制 (agent 在对话中用 browser_record save <name> 创建,存储于 $DSH_HOME/.dsh-browser/recordings)`),p?(0,r.createElement)(`div`,{style:s.empty},`加载失败: ${p}`):d===null?(0,r.createElement)(`div`,{style:s.empty},`加载中…`):d.length===0?(0,r.createElement)(`div`,{style:s.empty},`暂无录制。让 agent 跑完一轮流程后 browser_record save <name> 创建。`):d.map(e=>{let t=h[e.name];return(0,r.createElement)(`div`,{key:e.name,style:s.itemBox},(0,r.createElement)(`div`,{style:s.row},(0,r.createElement)(`span`,{style:s.label,title:`点击展开步骤`,onClick:()=>_(e.name)},e.name),(0,r.createElement)(`span`,{style:s.meta},`${e.steps} 步 · ${new Date(e.savedAt).toLocaleString()}`),(0,r.createElement)(`button`,{style:s.btn,onClick:()=>_(e.name)},t===void 0?`展开`:`收起`),(0,r.createElement)(`button`,{style:s.delBtn,onClick:()=>v(e.name)},`删除`)),t===void 0?null:t===`loading`?(0,r.createElement)(`div`,{style:s.empty},`加载中…`):(0,r.createElement)(`div`,{style:s.stepsBox},t===`failed`?(0,r.createElement)(`div`,{style:s.empty},`加载失败`):t.steps.length===0?(0,r.createElement)(`div`,{style:s.empty},`(空步骤)`):t.steps.map((t,n)=>(0,r.createElement)(`div`,{key:`${e.name}-${n}`,style:s.stepLine,title:`${t.tool} ${JSON.stringify(t.args??{})}`},(0,r.createElement)(`span`,{style:s.meta},`#${n}`),(0,r.createElement)(`span`,{style:s.toolName},t.tool),(0,r.createElement)(`span`,{style:s.stepArgs},JSON.stringify(t.args??{}))))))}),d!==null&&(0,r.createElement)(`div`,{style:s.heading},`提示:agent 在对话中用 browser_record save <name> 创建录制。`))}let d=[`slots`];function f(e){e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`browser`,order:40,label:`🌐 浏览器`},u)),e.slots.inject(`shell.overlay`,()=>e.slots.register({name:`shell.overlay`,id:`dsh-browser-overlay`,order:100},l))}return n.apply=f,n.inject=d,t.exports}});
|
|
2
2
|
//# sourceMappingURL=client.js.map
|