Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,30 @@ npm run build
| `evaluate_script` | 在页面或断点上下文执行 JavaScript,支持主世界、保存结果和读取一个本地输入文件 |
| `list_console_messages` | 列出控制台消息,或按 msgid 获取单条详情 |

## GUI 工作流脚本

除了 MCP Server 模式(CLI),项目还提供独立的 GUI 自动化脚本,用于浏览器操作、API 发现和数据采集。**GUI 和 CLI 共享同一套 CDP 连接层,可以同时使用。**

| 脚本 | 用途 |
|------|------|
| `scripts/gui/cdp-client.mjs` | CDP 连接层(公共模块,GUI/CLI 共享) |
| `scripts/gui/discover-apis.mjs` | API 端点发现 |
| `scripts/gui/intercept-response.mjs` | 网络响应拦截与篡改 |
| `scripts/gui/bulk-collect.mjs` | 批量分页数据采集 |

```bash
# API 发现
node scripts/gui/discover-apis.mjs --url https://example.com --port 9222

# 响应拦截(绕过地域/身份检测)
node scripts/gui/intercept-response.mjs --url https://example.com --pattern *api* --transform 'data.userProvince="湖北"'

# 批量采集
node scripts/gui/bulk-collect.mjs --url "https://api.example.com/list?page=" --port 9222 --pages 10 --output data.json
```

详见 [scripts/gui/TODO.md](scripts/gui/TODO.md)。

## 使用示例

### JS 逆向基本流程
Expand Down
31 changes: 31 additions & 0 deletions scripts/gui/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# GUI 工作流脚本 — 后续规划

已完成的基础模块:

- [x] cdp-client.mjs — CDP 连接层(GUI/CLI 共享)
- [x] discover-apis.mjs — API 端点发现
- [x] intercept-response.mjs — 网络响应拦截与篡改
- [x] bulk-collect.mjs — 批量分页采集

待完善:

- [ ] page-navigate.mjs — 多步骤导航链(支持条件等待、重试)
- [ ] capture-screenshot.mjs — 自动截图(可调用 MCP Server 的 take_screenshot 复用)
- [ ] extract-table.mjs — 从页面提取表格数据
- [ ] evaluate-flow.mjs — 执行 JS 并跟踪返回值变化
- [ ] export-to-curl.mjs — 将捕获的请求导出为 curl 命令

与 MCP Server 整合:

- [ ] 在 package.json 中添加 `scripts/gui/` 路径
- [ ] 统一 CLI 参数格式(与 MCP CLI 的 flag 风格对齐)
- [ ] 新增 `--gui` 启动参数,启动后进入交互式 GUI 模式提示

## 设计理念

GUI 脚本遵循以下原则:

1. **不重复实现 MCP Server 已有功能** — 断点、源码分析、Console 使用 MCP Server 的 21 个工具
2. **提供更高层的工作流封装** — API 发现、批量采集、响应拦截是跨场景的通用步骤
3. **与 CLI 模式共享 CDP 连接** — 所有脚本都基于 `cdp-client.mjs`,该模块也可被 MCP Server 内部引用
4. **浏览器实例复用** — GUI 脚本和 MCP Server 可以连接同一个浏览器,互不干扰
75 changes: 75 additions & 0 deletions scripts/gui/bulk-collect.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env node
/**
* 批量数据采集工具 - GUI 模式
*
* 从分页 API 批量采集数据,支持自动翻页和断点续采。
*
* 用法:
* node scripts/gui/bulk-collect.mjs --url "https://api.example.com/list?page=" --port 9222 --pages 10
* node scripts/gui/bulk-collect.mjs --url "https://api.example.com/list" --port 9222 --pages all
*/

import { CDPClient, findPageWs } from './cdp-client.mjs';
import fs from 'fs';

const args = process.argv.slice(2);
const targetUrl = args.includes('--url') ? args[args.indexOf('--url') + 1] : '';
const cdpPort = args.includes('--port') ? args[args.indexOf('--port') + 1] : '9222';
const pageParam = args.includes('--pages') ? args[args.indexOf('--pages') + 1] : '5';
const outputFile = args.includes('--output') ? args[args.indexOf('--output') + 1] : 'collected_data.json';
const delay = parseInt(args.includes('--delay') ? args[args.indexOf('--delay') + 1] : '500');

async function main() {
if (!targetUrl) { console.error('Usage: --url <url>'); process.exit(1); }

const wsUrl = await findPageWs(`http://127.0.0.1:${cdpPort}`, targetUrl);
if (!wsUrl) { console.error('No page found'); process.exit(1); }
const cdp = new CDPClient(wsUrl);
await cdp.send('Network.enable');

// 收集响应
const responses = [];
const pendingResponses = new Map();

cdp._ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.method === 'Network.responseReceived') {
const url = msg.params?.response?.url || '';
if (url.includes(targetUrl.split('?')[0])) {
const reqId = msg.params.requestId;
setTimeout(async () => {
try {
const body = await cdp.send('Network.getResponseBody', { requestId: reqId });
if (body?.result?.body) {
try {
responses.push(JSON.parse(body.result.body));
} catch(e) {
responses.push({ raw: body.result.body.substring(0, 500) });
}
}
} catch(e) {}
}, delay);
}
}
} catch(e) {}
});

// 计算页数
const pages = pageParam === 'all' ? 100 : parseInt(pageParam);
const hasPagePlaceholder = targetUrl.includes('page=');

for (let p = 1; p <= pages; p++) {
const url = hasPagePlaceholder ? targetUrl.replace(/page=\d+/, `page=${p}`) : `${targetUrl}${p}`;
console.log(`Page ${p}/${pages}...`);
await cdp.send('Page.navigate', { url });
await CDPClient.sleep(delay * 2);
}

// 保存结果
fs.writeFileSync(outputFile, JSON.stringify(responses, null, 2));
console.log(`\nDone! ${responses.length} responses saved to ${outputFile}`);
process.exit(0);
}

main().catch(e => { console.error('Error:', e); process.exit(1); });
101 changes: 101 additions & 0 deletions scripts/gui/cdp-client.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* CDP Client - 浏览器 DevTools Protocol 连接层
*
* GUI/CLI 共享模块。MCP Server 内部和 GUI 脚本均使用此模块连接和操作浏览器。
*
* 用法:
* import { CDPClient } from './cdp-client.mjs';
* const client = new CDPClient('ws://127.0.0.1:9222/...');
* await client.send('Page.navigate', { url: 'https://example.com' });
* const title = await client.eval('document.title');
*/

import WebSocket from 'ws';

export class CDPClient {
constructor(wsUrl) {
this._id = 1;
this._pending = new Map();
this._ws = new WebSocket(wsUrl);
return new Promise((resolve, reject) => {
this._ws.on('open', () => resolve(this));
this._ws.on('error', reject);
this._ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
const resolve = this._pending.get(msg.id);
if (resolve) { this._pending.delete(msg.id); resolve(msg); }
} catch(e) {}
});
});
}

/** 调用 CDP 方法 */
async send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = this._id++;
const timer = setTimeout(() => { this._pending.delete(id); reject(new Error(`Timeout: ${method}`)); }, 30000);
this._pending.set(id, (msg) => { clearTimeout(timer); resolve(msg); });
this._ws.send(JSON.stringify({ id, method, params }));
});
}

/** 在页面执行 JS */
async eval(expression) {
const r = await this.send('Runtime.evaluate', { expression, returnByValue: true });
return r?.result?.result?.value;
}

/** 模拟鼠标点击 */
async clickAt(x, y) {
await this.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
await this.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
}

/** 按 CSS 选择器点击 */
async clickEl(selector) {
const coords = await this.eval(`JSON.stringify((()=>{
const el = document.querySelector('${selector}');
if (!el) return null;
const rect = el.getBoundingClientRect();
return { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
})())`);
if (!coords) throw new Error(`Element not found: ${selector}`);
const { x, y } = JSON.parse(coords);
await this.clickAt(x, y);
}

/** 按文本内容点击 */
async clickByText(text) {
const coords = await this.eval(`JSON.stringify((()=>{
const all = document.querySelectorAll('*');
for (const el of all) {
if (el.textContent.trim() === '${text}' && el.offsetParent !== null) {
const rect = el.getBoundingClientRect();
return { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
}
}
return null;
})())`);
if (!coords) throw new Error(`Text not found: ${text}`);
const { x, y } = JSON.parse(coords);
await this.clickAt(x, y);
}

/** 等待指定毫秒 */
static sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
}

/** 从 CDP HTTP 端点获取页面 WebSocket URL */
export async function findPageWs(httpEndpoint, urlFilter) {
const resp = await fetch(`${httpEndpoint}/json`);
const pages = await resp.json();
const target = pages.find(p => p.url && (!urlFilter || p.url.includes(urlFilter)));
return target?.webSocketDebuggerUrl;
}

/** 从 CDP HTTP 端点获取所有页面 */
export async function listPages(httpEndpoint) {
const resp = await fetch(`${httpEndpoint}/json`);
return resp.json();
}
70 changes: 70 additions & 0 deletions scripts/gui/discover-apis.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env node
/**
* API 发现工具 - GUI 模式
*
* 连接到浏览器,导航到目标页面,自动捕获所有网络请求中的 API 端点。
*
* 用法:
* node scripts/gui/discover-apis.mjs --url https://example.com --port 9222
* node scripts/gui/discover-apis.mjs --url https://example.com --filter api/
*/

import { CDPClient, findPageWs } from './cdp-client.mjs';

const args = process.argv.slice(2);
const targetUrl = args.includes('--url') ? args[args.indexOf('--url') + 1] : 'https://example.com';
const cdpPort = args.includes('--port') ? args[args.indexOf('--port') + 1] : '9222';
const urlFilter = args.includes('--filter') ? args[args.indexOf('--filter') + 1] : 'api/';

async function main() {
// 1. 连接浏览器
const wsUrl = await findPageWs(`http://127.0.0.1:${cdpPort}`, targetUrl);
if (!wsUrl) {
console.error(`No page found on port ${cdpPort}`);
process.exit(1);
}
const cdp = new CDPClient(wsUrl);

// 2. 启用网络捕获
await cdp.send('Network.enable');

// 3. 收集 API 请求
const apis = new Map();
cdp._ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.method === 'Network.requestWillBeSent') {
const url = msg.params?.request?.url || '';
if (url.includes(urlFilter)) {
const key = url.split('?')[0];
if (!apis.has(key)) {
apis.set(key, {
method: msg.params.request.method,
url: key,
headers: msg.params.request.headers,
postData: msg.params.request.postData,
});
}
}
}
} catch(e) {}
});

// 4. 导航到目标页面
console.log(`Navigating to ${targetUrl}...`);
await cdp.send('Page.navigate', { url: targetUrl });
await CDPClient.sleep(5000);

// 5. 输出结果
console.log(`\nFound ${apis.size} API endpoints:`);
for (const [url, info] of apis) {
console.log(` ${info.method} ${url}`);
if (info.postData) {
console.log(` POST body: ${info.postData.substring(0, 200)}`);
}
}

process.exit(0);
}

main().catch(e => { console.error('Error:', e); process.exit(1); });
81 changes: 81 additions & 0 deletions scripts/gui/intercept-response.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* 网络响应拦截工具 - GUI 模式
*
* 拦截页面 API 请求的响应,实时修改后返回给页面。
* 可用于绕过服务端地域/身份检测,或模拟不同响应场景。
*
* 用法:
* node scripts/gui/intercept-response.mjs --url https://example.com --port 9222 --pattern *target-api* --transform 'data.userProvince="湖北"'
*/

import { CDPClient, findPageWs } from './cdp-client.mjs';

const args = process.argv.slice(2);
const targetUrl = args.includes('--url') ? args[args.indexOf('--url') + 1] : 'https://example.com';
const cdpPort = args.includes('--port') ? args[args.indexOf('--port') + 1] : '9222';
const urlPattern = args.includes('--pattern') ? args[args.indexOf('--pattern') + 1] : '*';
const transformExpr = args.includes('--transform') ? args[args.indexOf('--transform') + 1] : '';

async function main() {
const wsUrl = await findPageWs(`http://127.0.0.1:${cdpPort}`, targetUrl);
if (!wsUrl) { console.error(`No page found`); process.exit(1); }
const cdp = new CDPClient(wsUrl);

// 启用请求拦截
await cdp.send('Fetch.enable', {
patterns: [{ urlPattern, requestStage: 'Response' }]
});

let interceptedCount = 0;

cdp._ws.on('message', async (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.method === 'Fetch.requestPaused') {
const reqId = msg.params.requestId;
const url = msg.params.request?.url || '';

if (transformExpr) {
// 获取原始响应体
const bodyResp = await cdp.send('Fetch.getResponseBody', { requestId: reqId });
let responseBody = bodyResp?.result?.body || '{}';

try {
const parsed = JSON.parse(responseBody);
// 应用变换(简化版:通过 Function 执行用户定义的变换表达式)
const transform = new Function('data', `try { ${transformExpr}; return data; } catch(e) { return data; }`);
const modified = transform(parsed);
responseBody = JSON.stringify(modified);
interceptedCount++;
console.log(` Intercepted: ${url.substring(0, 80)}`);
} catch(e) {}

await cdp.send('Fetch.fulfillRequest', {
requestId: reqId,
responseCode: 200,
responseHeaders: [{ name: 'Content-Type', value: 'application/json; charset=utf-8' }],
body: Buffer.from(responseBody).toString('base64')
});
} else {
await cdp.send('Fetch.continueRequest', { requestId: reqId });
}
}
} catch(e) {}
});

// 导航到页面
console.log(`Navigating to ${targetUrl}...`);
await cdp.send('Page.navigate', { url: targetUrl });
await CDPClient.sleep(10000);

console.log(`\nIntercepted ${interceptedCount} requests`);
if (!transformExpr) {
console.log('Tip: Use --transform to modify responses');
console.log(' node scripts/gui/intercept-response.mjs --url URL --pattern *api* --transform \'data.userProvince="湖北"\'');
}

process.exit(0);
}

main().catch(e => { console.error('Error:', e); process.exit(1); });