@taole/dd-playwright 0.1.0 → 0.1.2
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/SKILL.md +19 -100
- package/bin/mcp-server.mjs +1 -1
- package/package.json +1 -1
- package/src/auth/diandian-auth.mjs +2 -2
- package/src/browser-check.mjs +103 -0
- package/src/browser-manager.mjs +2 -0
- package/src/server/web-server.mjs +187 -125
package/SKILL.md
CHANGED
|
@@ -1,109 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dd-playwright
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
当用户提到点点页面测试、免登访问、浏览器截图、控制台报错排查、本地 dev server 代理映射、dd-playwright、dd-pw、dd-browser 时使用。
|
|
4
|
+
通过 dd-playwright MCP 服务(提供 browser_* 工具集)进行点点前端业务页面与 H5 活动的自动化测试、免登导航、页面截图、表单交互与 Whistle 代理调试。
|
|
5
|
+
当用户提到点点页面测试、免登访问、浏览器截图、控制台报错排查、本地 dev server 代理映射、dd-playwright、dd-pw 时使用。
|
|
7
6
|
---
|
|
8
7
|
|
|
9
|
-
# dd-playwright
|
|
8
|
+
# dd-playwright 自动化指南
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
本技能必须通过 **`dd-playwright` MCP 服务**提供的 `browser_*` 系列工具执行。
|
|
12
11
|
|
|
13
|
-
##
|
|
12
|
+
## 1. 登录态与身份切换
|
|
13
|
+
- **默认免登**:访问点点业务页面必须调用本 MCP 的 `browser_navigate` 工具,自动注入当前激活账号的凭据(包含 `Tuwan_Passport` 等 Cookie)。
|
|
14
|
+
- **测试游客态/未登录拦截**:调用 `browser_navigate` 时必须显式指定 `anonymous: true`,用于验证未登录弹窗、游客导流或未鉴权状态。
|
|
15
|
+
- **指定测试账号**:传入特定账号的 `uid`(如 `browser_navigate({ url, uid: "28689781" })`),自动隔离并切换独立浏览器上下文。
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- 进程内完全隔离临时 Whistle 配置,绝不污染系统或用户目录 `~/.WhistleAppData`。
|
|
23
|
-
4. **一键 Web 扫码绑定**:
|
|
24
|
-
- 内置 Web 控制台(默认监听 `12309` 端口),手机扫码即刻完成账号绑定并同步至 MCP。
|
|
17
|
+
## 2. 本地开发服务联调(Dev Server 代理映射)
|
|
18
|
+
- 当本地启动了前端开发服务器(如 Vite/Webpack 监听本地端口)时,**严禁修改系统 hosts**。
|
|
19
|
+
- 必须先调用本 MCP 的 `browser_set_proxy_rule` 将本地端口映射到合法的点点子域名(子域名不要求在线上真实存在,支持任意自定义前缀)。
|
|
20
|
+
- **示例**:
|
|
21
|
+
- 本地 dev server 运行在 `127.0.0.1:9527` 时,可映射为自定义域名 `p9527-test.tuwan.com`:
|
|
22
|
+
`browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })`
|
|
23
|
+
- 随后调用 `browser_navigate({ url: "http://p9527-test.tuwan.com/" })` 访问,进程内隔离 Whistle 代理会自动转发流量并正确携带点点线上 Cookie。
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
在你的 AI 客户端(如 **omp**, **Cursor**, **Claude Desktop** 等)的 MCP 配置文件中添加:
|
|
31
|
-
|
|
32
|
-
### 1. 全局命令方式(推荐)
|
|
33
|
-
```json
|
|
34
|
-
{
|
|
35
|
-
"mcpServers": {
|
|
36
|
-
"dd-playwright": {
|
|
37
|
-
"command": "dd-playwright-mcp"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### 2. 源码路径方式(本地调试)
|
|
44
|
-
```json
|
|
45
|
-
{
|
|
46
|
-
"mcpServers": {
|
|
47
|
-
"dd-playwright": {
|
|
48
|
-
"command": "node",
|
|
49
|
-
"args": ["D:/tools/dd-browser-mcp/bin/mcp-server.mjs"]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## 工具集 (Tools) 契约
|
|
58
|
-
|
|
59
|
-
| 工具名称 | 关键参数 | 说明 |
|
|
60
|
-
|---|---|---|
|
|
61
|
-
| `browser_navigate` | `url` (必填)<br>`uid` (可选): 指定点点账号 UID<br>`anonymous` (可选): `true` 模拟游客态<br>`waitFor` (可选): 等待的 DOM 选择器<br>`timeout` (可选, 默认 15000ms) | 访问目标页面,返回 HTTP 状态码、页面标题与控制台报错统计。自动注入点点 storageState。不同身份调用自动隔离上下文。 |
|
|
62
|
-
| `browser_screenshot` | `selector` (可选): 指定元素选择器<br>`fullPage` (可选, 默认 false): 整页滚动截图 | 截取页面或特定元素,落盘到数据目录,返回本地图片路径。 |
|
|
63
|
-
| `browser_get_console_errors` | 无 | 收集当前页面自导航以来的 console.error 与未捕获异常。 |
|
|
64
|
-
| `browser_click_element` | `selector` (必填)<br>`timeout` (可选, 默认 10000ms) | 模拟用户点击指定 DOM 元素(按钮、选项卡等)。 |
|
|
65
|
-
| `browser_fill_text` | `selector` (必填)<br>`text` (必填)<br>`timeout` (可选) | 向目标输入框/文本域填入文本,原生触发 input 与 change 事件。 |
|
|
66
|
-
| `browser_press_key` | `key` (必填, 如 "Enter", "Tab", "Escape")<br>`selector` (可选): 目标聚焦元素 | 模拟键盘单键或组合键按下。 |
|
|
67
|
-
| `browser_hover` | `selector` (必填)<br>`timeout` (可选) | 鼠标悬停在指定 DOM 元素上,触发 hover 样式或 Tooltip 浮层。 |
|
|
68
|
-
| `browser_scroll` | `direction` ("down" 或 "up")<br>`deltaY` (可选数值)<br>`selector` (可选): 指定滚动元素 | 滚动视口或将指定元素滚动至可视区域。 |
|
|
69
|
-
| `browser_wait_for` | `selector` (可选)<br>`state` ("visible", "hidden", "attached", "detached")<br>`timeout` (可选) | 等待页面加载状态或特定 DOM 元素出现/隐藏。 |
|
|
70
|
-
| `browser_get_dom` | `interactiveOnly` (可选, 默认 true)<br>`maxDepth` (可选, 默认 10) | 结构化提取页面可见交互元素清单(含文本与选择器),免去盲猜或截大图。 |
|
|
71
|
-
| `browser_eval_js` | `script` (必填): JS 代码字符串 | 在当前浏览器页面上下文中执行任意 JavaScript 脚本并返回结果。 |
|
|
72
|
-
| `browser_set_proxy_rule` | `domain` (必填, 需匹配 `*.tuwan.com`)<br>`port` (必填, 本地服务端口)<br>`protocol` (可选, "http" 或 "https") | 声明本地 dev server 代理映射,使本地开发服务可通过合法的点点域名访问并正常携带登录 Cookie。 |
|
|
73
|
-
| `browser_status` | 无 | 查询当前服务版本、激活点点账号、代理状态与数据目录。 |
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Web 账号控制台 (扫码绑定)
|
|
78
|
-
|
|
79
|
-
在终端运行以下命令启动管理控制台:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
# 后台静默启动(推荐,不占控制台终端):
|
|
83
|
-
dd-pw startweb
|
|
84
|
-
|
|
85
|
-
# 查看后台运行状态与 PID:
|
|
86
|
-
dd-pw statusweb
|
|
87
|
-
|
|
88
|
-
# 停止后台服务:
|
|
89
|
-
dd-pw stopweb
|
|
90
|
-
|
|
91
|
-
# 前台阻塞运行(Ctrl+C 退出):
|
|
92
|
-
dd-pw serve
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
- 打开浏览器访问:`http://127.0.0.1:12309`
|
|
96
|
-
- 点击 **「+ 添加绑定账号」**,使用微信或点点 App 扫描二维码即可。
|
|
97
|
-
- 控制台支持多账号查看、设为默认账号或解绑账号。
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## Agent 使用最佳实践
|
|
102
|
-
|
|
103
|
-
1. **先排查状态**:在执行复杂的业务排查或测试前,先调用 `browser_status` 确认是否已加载登录态或默认账号 UID。
|
|
104
|
-
2. **测试登录态与未登录态对比**:
|
|
105
|
-
- 先调用 `browser_navigate({ url, anonymous: true })` 查看游客视角行为(如是否弹出登录拦截、展示游客文案)。
|
|
106
|
-
- 再调用 `browser_navigate({ url })` 查看默认登录用户视角;或 `browser_navigate({ url, uid: "..." })` 验证特定业务角色。
|
|
107
|
-
3. **本地 dev 调试**:
|
|
108
|
-
- 本地前端开发服务启动后(例如 `127.0.0.1:9527`),先调用 `browser_set_proxy_rule({ domain: "act-dev.tuwan.com", port: 9527 })`。
|
|
109
|
-
- 随后调用 `browser_navigate({ url: "http://act-dev.tuwan.com/..." })`,不仅不改 hosts,且浏览器能正常携带点点线上 Cookie。
|
|
25
|
+
## 3. 页面交互规范
|
|
26
|
+
- **元素定位**:在动态页面或选择器不确定时,优先调用 `browser_get_dom` 提取当前可见的交互元素结构(包含按钮、输入框、链接及其选择器),严禁盲猜选择器。
|
|
27
|
+
- **表单与按键**:表单输入调用 `browser_fill_text`(原生触发 `input` 与 `change` 事件);键盘按键/回车提交调用 `browser_press_key`。
|
|
28
|
+
- **状态排查**:排查免登异常前先调用 `browser_status` 确认 MCP 服务凭据挂载状态与当前 UID。
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveConfig } from "../src/config.mjs";
|
|
|
10
10
|
import { PlaywrightBrowserManager } from "../src/browser-manager.mjs";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
12
|
|
|
13
|
-
export const MCP_VERSION = "0.1.
|
|
13
|
+
export const MCP_VERSION = "0.1.1";
|
|
14
14
|
|
|
15
15
|
function textResult(payload) {
|
|
16
16
|
return {
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
|
+
import { ensureChromiumInstalled } from "../browser-check.mjs";
|
|
2
3
|
|
|
3
4
|
export const DIANDIAN_HOME_URL = "https://y.tuwan.com";
|
|
4
5
|
export const DIANDIAN_USER_INFO_URL =
|
|
@@ -43,10 +44,9 @@ export class DiandianAuthManager {
|
|
|
43
44
|
|
|
44
45
|
this.startupPromise = (async () => {
|
|
45
46
|
await this.close();
|
|
46
|
-
|
|
47
|
+
await ensureChromiumInstalled();
|
|
47
48
|
const instance = await this.chromium.launch({ headless });
|
|
48
49
|
const ctx = await instance.newContext({
|
|
49
|
-
viewport: { width: 1100, height: 850 },
|
|
50
50
|
userAgent:
|
|
51
51
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
52
52
|
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
let cachedChromiumMeta = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取当前 Chromium 内核安装状态与元数据
|
|
9
|
+
* @returns {Promise<{ installed: boolean, executablePath: string, version: string|null, error: string|null }>}
|
|
10
|
+
*/
|
|
11
|
+
export async function getChromiumMetadata() {
|
|
12
|
+
let executablePath = "";
|
|
13
|
+
try {
|
|
14
|
+
executablePath = chromium.executablePath();
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return {
|
|
17
|
+
installed: false,
|
|
18
|
+
executablePath: "",
|
|
19
|
+
version: null,
|
|
20
|
+
error: err.message,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const installed = Boolean(executablePath && existsSync(executablePath));
|
|
25
|
+
if (!installed) {
|
|
26
|
+
return {
|
|
27
|
+
installed: false,
|
|
28
|
+
executablePath: executablePath || "",
|
|
29
|
+
version: null,
|
|
30
|
+
error: "未在系统默认目录中发现对应版本的 Chromium 二进制文件",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (cachedChromiumMeta && cachedChromiumMeta.installed) {
|
|
35
|
+
return cachedChromiumMeta;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let version = null;
|
|
39
|
+
try {
|
|
40
|
+
const browser = await chromium.launch({ headless: true });
|
|
41
|
+
version = browser.version();
|
|
42
|
+
await browser.close();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// 无法正常启动时可能是缺少依赖库或路径损坏
|
|
45
|
+
return {
|
|
46
|
+
installed: true,
|
|
47
|
+
executablePath,
|
|
48
|
+
version: null,
|
|
49
|
+
error: `启动检查异常: ${err.message}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cachedChromiumMeta = {
|
|
54
|
+
installed: true,
|
|
55
|
+
executablePath,
|
|
56
|
+
version,
|
|
57
|
+
error: null,
|
|
58
|
+
};
|
|
59
|
+
return cachedChromiumMeta;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 确保 Chromium 内核已就绪,若缺失则自动执行下载补齐
|
|
64
|
+
* @param {object} [options]
|
|
65
|
+
* @param {boolean} [options.silent] 是否静默输出
|
|
66
|
+
* @returns {Promise<boolean>} 是否成功就绪
|
|
67
|
+
*/
|
|
68
|
+
export async function ensureChromiumInstalled({ silent = false } = {}) {
|
|
69
|
+
const meta = await getChromiumMetadata();
|
|
70
|
+
if (meta.installed && meta.version) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!silent) {
|
|
75
|
+
console.log(
|
|
76
|
+
"\n[dd-playwright] 检测到当前系统尚未安装 Chromium 浏览器内核,正在为您自动下载 (~150MB),请稍候...",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// 调用 playwright 内置命令安装 chromium
|
|
82
|
+
execSync("npx playwright install chromium", {
|
|
83
|
+
stdio: silent ? "ignore" : "inherit",
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
});
|
|
86
|
+
cachedChromiumMeta = null; // 清理缓存
|
|
87
|
+
const verify = await getChromiumMetadata();
|
|
88
|
+
if (!silent) {
|
|
89
|
+
if (verify.installed) {
|
|
90
|
+
console.log(`[dd-playwright] Chromium 内核自动安装成功!版本: ${verify.version}\n`);
|
|
91
|
+
} else {
|
|
92
|
+
console.warn(`[dd-playwright] Chromium 安装流程已结束,但未能在路径检测到执行文件: ${verify.executablePath}\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return verify.installed;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (!silent) {
|
|
98
|
+
console.error(`[dd-playwright] 自动安装 Chromium 失败: ${err.message}`);
|
|
99
|
+
console.error(`可手动在终端执行: npx playwright install chromium\n`);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/browser-manager.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { chromium } from "playwright";
|
|
8
|
+
import { ensureChromiumInstalled } from "./browser-check.mjs";
|
|
8
9
|
import { WhistleProxyManager } from "./whistle-proxy.mjs";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -164,6 +165,7 @@ export class PlaywrightBrowserManager {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
if (!this.browser) {
|
|
168
|
+
await ensureChromiumInstalled();
|
|
167
169
|
const launchArgs = this.whistle.started ? this.whistle.getBrowserArgs() : [];
|
|
168
170
|
this.browser = await chromium.launch({
|
|
169
171
|
headless: this.headless,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { AccountStore } from "../account-store.mjs";
|
|
4
4
|
import { DiandianAuthManager } from "../auth/diandian-auth.mjs";
|
|
5
|
+
import { getChromiumMetadata } from "../browser-check.mjs";
|
|
5
6
|
|
|
6
7
|
function sendJson(res, statusCode, payload) {
|
|
7
8
|
res.writeHead(statusCode, {
|
|
@@ -300,7 +301,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
300
301
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 9h6"/></svg>
|
|
301
302
|
dd-playwright 账号管理控制台
|
|
302
303
|
</h1>
|
|
303
|
-
<span style="font-size: 13px; color: var(--muted);">
|
|
304
|
+
<span style="font-size: 13px; color: var(--muted);">v{{VERSION}}</span>
|
|
304
305
|
</header>
|
|
305
306
|
|
|
306
307
|
<div class="nav-tabs">
|
|
@@ -326,13 +327,18 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
326
327
|
<!-- TAB 2: MCP 配置说明 -->
|
|
327
328
|
<div id="pane-mcp" class="tab-pane">
|
|
328
329
|
<div class="card">
|
|
329
|
-
<div
|
|
330
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
331
|
+
<div class="card-title" style="margin-bottom: 0;">MCP 挂载配置指南</div>
|
|
332
|
+
<button class="btn" onclick="installToCursor()" style="background: #0284c7; color: white; display: flex; align-items: center; gap: 7px; font-weight: 600; padding: 7px 14px;">
|
|
333
|
+
<svg width="14" height="16" viewBox="0 0 467 533" fill="currentColor"><path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/></svg>
|
|
334
|
+
一键安装到 Cursor
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
330
337
|
|
|
331
338
|
<!-- 动态运行环境提示卡片 -->
|
|
332
339
|
<div id="envBanner" style="padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; font-size: 13px; line-height: 1.5; {{BANNER_STYLE}}">
|
|
333
340
|
{{BANNER_CONTENT}}
|
|
334
341
|
</div>
|
|
335
|
-
|
|
336
342
|
<!-- 配置建议区块 -->
|
|
337
343
|
{{CONFIG_SECTIONS}}
|
|
338
344
|
|
|
@@ -347,93 +353,42 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
347
353
|
<!-- TAB 3: Skill 规范与工具说明 -->
|
|
348
354
|
<div id="pane-skill" class="tab-pane">
|
|
349
355
|
<div class="card">
|
|
350
|
-
<div
|
|
351
|
-
|
|
356
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
357
|
+
<div class="card-title" style="margin-bottom: 0;">Skill 规范说明 (SKILL.md)</div>
|
|
358
|
+
<div style="display: flex; gap: 8px;">
|
|
359
|
+
<a href="/api/skill/download" download="SKILL.md" class="btn" style="background: #334155; color: white; display: flex; align-items: center; gap: 6px; font-weight: 500; padding: 7px 12px; text-decoration: none;">
|
|
360
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
361
|
+
下载 SKILL.md
|
|
362
|
+
</a>
|
|
363
|
+
<button class="btn" onclick="installSkillToCursor()" style="background: #0284c7; color: white; display: flex; align-items: center; gap: 7px; font-weight: 600; padding: 7px 14px;">
|
|
364
|
+
<svg width="14" height="16" viewBox="0 0 467 533" fill="currentColor"><path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/></svg>
|
|
365
|
+
一键安装到 Cursor
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
352
369
|
<div class="doc-section">
|
|
353
370
|
<h3>适用场景</h3>
|
|
354
|
-
<p
|
|
371
|
+
<p>适用于点点业务页面/活动 H5 自动化测试、免登导航、页面截图、控制台报错排查、本地 dev server 代理映射等任务。</p>
|
|
355
372
|
</div>
|
|
356
373
|
|
|
357
374
|
<div class="doc-section">
|
|
358
|
-
<h3
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
</
|
|
367
|
-
<
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
<td><code>browser_screenshot</code></td>
|
|
375
|
-
<td><code>selector</code> (可选)<br><code>fullPage</code> (可选,默认 false)</td>
|
|
376
|
-
<td>截取指定元素或当前整页,保存至本地图片文件并返回绝对路径。</td>
|
|
377
|
-
</tr>
|
|
378
|
-
<tr>
|
|
379
|
-
<td><code>browser_get_console_errors</code></td>
|
|
380
|
-
<td>(无参数)</td>
|
|
381
|
-
<td>返回自导航以来的 console.error 与未捕获异常列表,方便诊断页面 JS 报错。</td>
|
|
382
|
-
</tr>
|
|
383
|
-
<tr>
|
|
384
|
-
<td><code>browser_click_element</code></td>
|
|
385
|
-
<td><code>selector</code> (必填)<br><code>timeout</code> (可选)</td>
|
|
386
|
-
<td>在页面中按 CSS/XPath 选择器模拟点击元素(按钮、Tab、确认框等)。</td>
|
|
387
|
-
</tr>
|
|
388
|
-
<tr>
|
|
389
|
-
<td><code>browser_fill_text</code></td>
|
|
390
|
-
<td><code>selector</code> (必填)<br><code>text</code> (必填)<br><code>timeout</code> (可选)</td>
|
|
391
|
-
<td>向输入框填入文本,触发 input 与 change 事件。</td>
|
|
392
|
-
</tr>
|
|
393
|
-
<tr>
|
|
394
|
-
<td><code>browser_press_key</code></td>
|
|
395
|
-
<td><code>key</code> (必填, 如 Enter, Tab, Escape)<br><code>selector</code> (可选)</td>
|
|
396
|
-
<td>模拟键盘按键,可聚焦特定元素按键或向全局页面按键。</td>
|
|
397
|
-
</tr>
|
|
398
|
-
<tr>
|
|
399
|
-
<td><code>browser_hover</code></td>
|
|
400
|
-
<td><code>selector</code> (必填)<br><code>timeout</code> (可选)</td>
|
|
401
|
-
<td>鼠标悬停在指定 DOM 元素上,触发 hover 菜单或 Tooltip 浮层。</td>
|
|
402
|
-
</tr>
|
|
403
|
-
<tr>
|
|
404
|
-
<td><code>browser_scroll</code></td>
|
|
405
|
-
<td><code>direction</code> ("down" / "up")<br><code>deltaY</code> (可选)<br><code>selector</code> (可选)</td>
|
|
406
|
-
<td>视口滚动或滚动指定元素进入视图(触底加载等)。</td>
|
|
407
|
-
</tr>
|
|
408
|
-
<tr>
|
|
409
|
-
<td><code>browser_wait_for</code></td>
|
|
410
|
-
<td><code>selector</code> (可选)<br><code>state</code> ("visible" 等)<br><code>timeout</code> (可选)</td>
|
|
411
|
-
<td>等待页面整页加载或特定 DOM 元素出现/隐藏。</td>
|
|
412
|
-
</tr>
|
|
413
|
-
<tr>
|
|
414
|
-
<td><code>browser_get_dom</code></td>
|
|
415
|
-
<td><code>interactiveOnly</code> (默认 true)<br><code>maxDepth</code> (默认 10)</td>
|
|
416
|
-
<td>提取当前页面结构化可交互 DOM 树(按钮/输入框/链接及其选择器),免去盲猜。</td>
|
|
417
|
-
</tr>
|
|
418
|
-
<tr>
|
|
419
|
-
<td><code>browser_eval_js</code></td>
|
|
420
|
-
<td><code>script</code> (必填,JS 字符串)</td>
|
|
421
|
-
<td>在当前页面上下文中执行任意 JavaScript 脚本并返回计算结果。</td>
|
|
422
|
-
</tr>
|
|
423
|
-
<tr>
|
|
424
|
-
<td><code>browser_set_proxy_rule</code></td>
|
|
425
|
-
<td><code>domain</code> (需匹配 *.tuwan.com)<br><code>port</code> (本地服务端口)<br><code>protocol</code> (http/https)</td>
|
|
426
|
-
<td>启动/设置本进程内隔离 Whistle 代理,将本地端口映射到干净点点域名,免改 hosts 携带 Cookie。</td>
|
|
427
|
-
</tr>
|
|
428
|
-
<tr>
|
|
429
|
-
<td><code>browser_status</code></td>
|
|
430
|
-
<td>(无参数)</td>
|
|
431
|
-
<td>查询当前 MCP 状态:版本号、默认账号 UID、storageState 加载情况、代理规则与数据目录。</td>
|
|
432
|
-
</tr>
|
|
433
|
-
</tbody>
|
|
434
|
-
</table>
|
|
375
|
+
<h3>自动化执行指导准则</h3>
|
|
376
|
+
<p>本技能必须通过 <b><code>dd-playwright</code> MCP 服务</b>(提供 <code>browser_*</code> 工具集)执行:</p>
|
|
377
|
+
|
|
378
|
+
<div style="background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 14px; margin-top: 10px; font-size: 13px; line-height: 1.8; color: #cbd5e1;">
|
|
379
|
+
<b>1. 登录态与身份切换:</b><br>
|
|
380
|
+
• 访问点点业务页面必须调用本 MCP 的 <code>browser_navigate</code> 工具,自动注入当前激活账号的 storageState(含 <code>Tuwan_Passport</code> 鉴权 Cookie)免登。<br>
|
|
381
|
+
• 多角色或指定账号测试时传入特定 <code>uid</code>,自动隔离并重建独立浏览器上下文。<br><br>
|
|
382
|
+
<b>2. 本地开发服务联调(Dev Server 代理映射):</b><br>
|
|
383
|
+
• 本地服务启动后严禁改动系统 hosts,必须先调用 <code>browser_set_proxy_rule</code> 映射到点点子域名。<br>
|
|
384
|
+
• <b>示例</b>:本地 dev server 运行在 <code>127.0.0.1:9527</code> 时,可映射为自定义域名 <code>p9527-test.tuwan.com</code>(域名无需线上真实存在):<br>
|
|
385
|
+
<code style="color: #38bdf8;">browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })</code><br>
|
|
386
|
+
随后访问该域名,内部隔离 Whistle 代理会自动转发流量并正确携带线上 Cookie。<br><br>
|
|
387
|
+
<b>3. 页面交互规范:</b><br>
|
|
388
|
+
• 复杂或动态页面定位困难时,优先调用 <code>browser_get_dom</code> 获取可见元素清单,严禁盲猜选择器。<br>
|
|
389
|
+
• 表单填写使用 <code>browser_fill_text</code>,按键使用 <code>browser_press_key</code>。
|
|
390
|
+
</div>
|
|
435
391
|
</div>
|
|
436
|
-
</div>
|
|
437
392
|
</div>
|
|
438
393
|
<!-- TAB 4: 全局配置 -->
|
|
439
394
|
<div id="pane-settings" class="tab-pane">
|
|
@@ -453,11 +408,25 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
453
408
|
<span id="headlessKnob" style="position: absolute; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .2s; border-radius: 50%;"></span>
|
|
454
409
|
</label>
|
|
455
410
|
</div>
|
|
456
|
-
|
|
457
411
|
<div style="margin-top: 16px; font-size: 12px; color: var(--muted); line-height: 1.6; padding: 12px; background: #090d16; border-radius: 6px; border: 1px solid var(--border);">
|
|
458
412
|
📁 <b>数据存储说明:</b><br>
|
|
459
413
|
本服务所有数据与账号库已默认固化在用户主目录下的 <code>~/.dd-browser-mcp/runtime.db</code>,无需配置任何复杂环境变量,开箱即用。
|
|
460
414
|
</div>
|
|
415
|
+
|
|
416
|
+
<!-- Chromium 浏览器内核状态展示卡片 -->
|
|
417
|
+
<div style="margin-top: 20px; padding: 16px; background: #0f172a; border-radius: 8px; border: 1px solid var(--border);">
|
|
418
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
419
|
+
<div style="font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
|
420
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/><line x1="21.17" y1="8" x2="12" y2="8"/><line x1="3.95" y1="6.06" x2="8.54" y2="14"/><line x1="10.88" y1="21.94" x2="15.46" y2="14"/></svg>
|
|
421
|
+
Chromium 浏览器内核
|
|
422
|
+
</div>
|
|
423
|
+
<span id="chromiumStatusBadge" style="font-size: 11px; padding: 2px 8px; border-radius: 4px; background: #334155; color: var(--muted);">检测中...</span>
|
|
424
|
+
</div>
|
|
425
|
+
<div id="chromiumDetails" style="font-size: 12px; color: var(--muted); line-height: 1.8;">
|
|
426
|
+
<div>版本号:<span id="chromiumVersion" style="color: #cbd5e1; font-family: monospace;">加载中...</span></div>
|
|
427
|
+
<div>执行路径:<span id="chromiumPath" style="color: #cbd5e1; font-family: monospace; word-break: break-all;">加载中...</span></div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
461
430
|
</div>
|
|
462
431
|
</div>
|
|
463
432
|
|
|
@@ -628,6 +597,53 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
628
597
|
});
|
|
629
598
|
}
|
|
630
599
|
}
|
|
600
|
+
function installToCursor() {
|
|
601
|
+
const serverName = 'dd-playwright';
|
|
602
|
+
let mcpConfigObj = {
|
|
603
|
+
command: 'node',
|
|
604
|
+
args: []
|
|
605
|
+
};
|
|
606
|
+
const localCode = document.getElementById('code-mcp-local')?.innerText;
|
|
607
|
+
if (localCode) {
|
|
608
|
+
try {
|
|
609
|
+
const parsed = JSON.parse(localCode);
|
|
610
|
+
const cfg = parsed.mcpServers?.[serverName];
|
|
611
|
+
if (cfg) {
|
|
612
|
+
mcpConfigObj = cfg;
|
|
613
|
+
}
|
|
614
|
+
} catch {}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Cursor 官方标准格式:
|
|
618
|
+
// 路由为 anysphere.cursor-deeplink/mcp/install
|
|
619
|
+
// config 参数需为 Base64 编码的 JSON 字符串
|
|
620
|
+
const jsonStr = JSON.stringify(mcpConfigObj);
|
|
621
|
+
// 处理 UTF-8 字符的 Base64 编码
|
|
622
|
+
const base64Config = btoa(encodeURIComponent(jsonStr).replace(/%([0-9A-F]{2})/g, (match, p1) => {
|
|
623
|
+
return String.fromCharCode('0x' + p1);
|
|
624
|
+
}));
|
|
625
|
+
const cursorUrl = 'cursor://anysphere.cursor-deeplink/mcp/install?name=' + encodeURIComponent(serverName) + '&config=' + encodeURIComponent(base64Config);
|
|
626
|
+
// 唤起 Cursor
|
|
627
|
+
window.location.href = cursorUrl;
|
|
628
|
+
}
|
|
629
|
+
function installSkillToCursor() {
|
|
630
|
+
const ruleName = 'dd-playwright';
|
|
631
|
+
const compactRule = [
|
|
632
|
+
'---',
|
|
633
|
+
'description: 通过 dd-playwright MCP 服务(提供 browser_* 工具集)进行点点前端业务自动化测试、免登调试、页面截图排查与 dev server 代理映射',
|
|
634
|
+
'globs: *',
|
|
635
|
+
'---',
|
|
636
|
+
'# dd-playwright 自动化指南',
|
|
637
|
+
'本技能必须通过 dd-playwright MCP 服务提供的 browser_* 系列工具执行:',
|
|
638
|
+
'1. 登录态与身份切换:访问点点页面调用本 MCP 的 browser_navigate 自动注入激活账号免登;排查游客态或未登录拦截必须传 anonymous: true;指定测试账号传特定 uid。',
|
|
639
|
+
'2. 本地 Dev Server 联调:严禁修改系统 hosts。先调用本 MCP 的 browser_set_proxy_rule 将本地端口映射到点点子域名(如 127.0.0.1:9527 映射为 p9527-test.tuwan.com),免改 hosts 携带线上 Cookie。',
|
|
640
|
+
'3. 页面交互规范:定位困难时优先调用 browser_get_dom 提取可见元素结构,严禁盲猜选择器;表单填写使用 browser_fill_text,按键使用 browser_press_key。'
|
|
641
|
+
].join('\\n');
|
|
642
|
+
|
|
643
|
+
const cursorUrl = 'cursor://anysphere.cursor-deeplink/rule?name=' + encodeURIComponent(ruleName) + '&text=' + encodeURIComponent(compactRule);
|
|
644
|
+
window.location.href = cursorUrl;
|
|
645
|
+
}
|
|
646
|
+
|
|
631
647
|
|
|
632
648
|
async function loadSettings() {
|
|
633
649
|
try {
|
|
@@ -667,9 +683,35 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
667
683
|
}
|
|
668
684
|
}
|
|
669
685
|
|
|
670
|
-
|
|
686
|
+
async function loadChromiumMeta() {
|
|
687
|
+
try {
|
|
688
|
+
const res = await fetch('/api/chromium');
|
|
689
|
+
const data = await res.json();
|
|
690
|
+
const badge = document.getElementById('chromiumStatusBadge');
|
|
691
|
+
const versionEl = document.getElementById('chromiumVersion');
|
|
692
|
+
const pathEl = document.getElementById('chromiumPath');
|
|
693
|
+
if (!badge || !versionEl || !pathEl) return;
|
|
694
|
+
|
|
695
|
+
if (data.ok && data.meta && data.meta.installed) {
|
|
696
|
+
badge.innerText = '已安装就绪';
|
|
697
|
+
badge.style.background = 'rgba(16, 185, 129, 0.15)';
|
|
698
|
+
badge.style.color = 'var(--success)';
|
|
699
|
+
versionEl.innerText = data.meta.version || '已安装 (Playwright 托管)';
|
|
700
|
+
pathEl.innerText = data.meta.executablePath || '-';
|
|
701
|
+
} else {
|
|
702
|
+
badge.innerText = '未就绪 (首次调用将自动安装)';
|
|
703
|
+
badge.style.background = 'rgba(239, 68, 68, 0.15)';
|
|
704
|
+
badge.style.color = 'var(--danger)';
|
|
705
|
+
versionEl.innerText = '未检测到内核';
|
|
706
|
+
pathEl.innerText = data.meta?.executablePath || '-';
|
|
707
|
+
}
|
|
708
|
+
} catch {}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// 初次加载账号、配置与内核状态
|
|
671
712
|
loadAccounts();
|
|
672
713
|
loadSettings();
|
|
714
|
+
loadChromiumMeta();
|
|
673
715
|
</script>
|
|
674
716
|
</body>
|
|
675
717
|
</html>`;
|
|
@@ -722,51 +764,29 @@ export function createWebServer({ accountStore, authManager }) {
|
|
|
722
764
|
2,
|
|
723
765
|
);
|
|
724
766
|
|
|
725
|
-
let bannerStyle = "";
|
|
726
|
-
let bannerContent = "";
|
|
727
|
-
let configSections = "";
|
|
767
|
+
let bannerStyle = "background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.3); color: #60a5fa;";
|
|
768
|
+
let bannerContent = `💻 <b>当前代码位置:</b><code style="background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; color: #93c5fd;">${serverScriptPath}</code><br><span style="color:#cbd5e1; font-size:12px; margin-top: 4px; display: inline-block;">使用 Node 绝对路径方式挂载 MCP,不受系统 PATH 或环境切换影响,100% 稳定运行。</span>`;
|
|
728
769
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
<button class="copy-btn" onclick="copyCode('code-mcp-global')">复制代码</button>
|
|
737
|
-
</div>
|
|
738
|
-
<div id="code-mcp-global" class="code-block">${globalConfigStr}</div>
|
|
739
|
-
|
|
740
|
-
<div class="code-title" style="margin-top: 24px;">
|
|
741
|
-
<span style="color: var(--muted);">备用方式:模块绝对路径方式</span>
|
|
742
|
-
<button class="copy-btn" onclick="copyCode('code-mcp-local')">复制代码</button>
|
|
743
|
-
</div>
|
|
744
|
-
<div id="code-mcp-local" class="code-block">${localConfigStr}</div>
|
|
745
|
-
`;
|
|
746
|
-
} else {
|
|
747
|
-
bannerStyle = "background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.3); color: #60a5fa;";
|
|
748
|
-
bannerContent = "💻 <b>环境检测结果:本地源码目录运行(开发/克隆模式)</b><br><span style='color:#cbd5e1; font-size:12px;'>已实时解析您当前的本地绝对路径,无需全局安装即可直接挂载;若希望使用全局命令,可在根目录执行一次 <code>npm link</code>。</span>";
|
|
749
|
-
|
|
750
|
-
configSections = `
|
|
751
|
-
<div class="code-title">
|
|
752
|
-
<span>推荐方式:本地源码绝对路径方式(免全局安装,当前代码实时生效)</span>
|
|
753
|
-
<button class="copy-btn" onclick="copyCode('code-mcp-local')">复制代码</button>
|
|
754
|
-
</div>
|
|
755
|
-
<div id="code-mcp-local" class="code-block">${localConfigStr}</div>
|
|
770
|
+
let configSections = `
|
|
771
|
+
<div class="code-title">
|
|
772
|
+
<span>本地绝对路径配置(推荐,开箱即用)</span>
|
|
773
|
+
<button class="copy-btn" onclick="copyCode('code-mcp-local')">复制代码</button>
|
|
774
|
+
</div>
|
|
775
|
+
<div id="code-mcp-local" class="code-block">${localConfigStr}</div>
|
|
776
|
+
`;
|
|
756
777
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
778
|
+
const pkgPath = fileURLToPath(new URL("../../package.json", import.meta.url));
|
|
779
|
+
let pkgVersion = "0.1.0";
|
|
780
|
+
try {
|
|
781
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
782
|
+
pkgVersion = pkg.version || "0.1.0";
|
|
783
|
+
} catch {}
|
|
764
784
|
|
|
765
785
|
const renderedHtml = DASHBOARD_HTML
|
|
786
|
+
.replace("{{VERSION}}", pkgVersion)
|
|
766
787
|
.replace("{{BANNER_STYLE}}", bannerStyle)
|
|
767
788
|
.replace("{{BANNER_CONTENT}}", bannerContent)
|
|
768
789
|
.replace("{{CONFIG_SECTIONS}}", configSections);
|
|
769
|
-
|
|
770
790
|
sendHtml(res, renderedHtml);
|
|
771
791
|
return;
|
|
772
792
|
}
|
|
@@ -782,6 +802,48 @@ export function createWebServer({ accountStore, authManager }) {
|
|
|
782
802
|
}
|
|
783
803
|
return;
|
|
784
804
|
}
|
|
805
|
+
// 2.1 获取 SKILL.md 原始内容
|
|
806
|
+
if (pathname === "/api/skill" && req.method === "GET") {
|
|
807
|
+
try {
|
|
808
|
+
const { readFileSync } = await import("node:fs");
|
|
809
|
+
const skillPath = fileURLToPath(new URL("../../SKILL.md", import.meta.url));
|
|
810
|
+
const content = readFileSync(skillPath, "utf8");
|
|
811
|
+
sendJson(res, 200, { ok: true, content });
|
|
812
|
+
} catch (err) {
|
|
813
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 2.2 下载 SKILL.md 附件
|
|
819
|
+
if (pathname === "/api/skill/download" && req.method === "GET") {
|
|
820
|
+
try {
|
|
821
|
+
const { readFileSync } = await import("node:fs");
|
|
822
|
+
const skillPath = fileURLToPath(new URL("../../SKILL.md", import.meta.url));
|
|
823
|
+
const content = readFileSync(skillPath, "utf8");
|
|
824
|
+
res.writeHead(200, {
|
|
825
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
826
|
+
"Content-Disposition": 'attachment; filename="SKILL.md"',
|
|
827
|
+
"Cache-Control": "no-cache",
|
|
828
|
+
});
|
|
829
|
+
res.end(content);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
// 2.3 获取 Chromium 内核元数据
|
|
836
|
+
if (pathname === "/api/chromium" && req.method === "GET") {
|
|
837
|
+
try {
|
|
838
|
+
const meta = await getChromiumMetadata();
|
|
839
|
+
sendJson(res, 200, { ok: true, meta });
|
|
840
|
+
} catch (err) {
|
|
841
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
|
|
785
847
|
|
|
786
848
|
// 3. 切换激活账号
|
|
787
849
|
if (pathname === "/api/switch" && req.method === "POST") {
|