validpilot-oss 1.1.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/CHANGELOG.md +111 -0
- package/README.md +196 -0
- package/bin/validpilot.js +173 -0
- package/brain/error_aggregator.js +203 -0
- package/core/artifacts.js +44 -0
- package/core/config.js +37 -0
- package/core/redaction.js +39 -0
- package/core/report.js +42 -0
- package/core/result.js +29 -0
- package/core/security.js +57 -0
- package/engines/chrome_mcp_adapter.js +319 -0
- package/engines/playwright_adapter.js +421 -0
- package/examples/demo/README.md +58 -0
- package/examples/demo/diagnostic-error-flow.json +22 -0
- package/examples/demo/diagnostic-error.html +29 -0
- package/examples/demo/flow.json +27 -0
- package/examples/demo/index.html +29 -0
- package/hands/browser_operator.js +67 -0
- package/hands/evidence_collector.js +97 -0
- package/package.json +55 -0
- package/rules/suggested-rules.json +237 -0
- package/server.js +5376 -0
- package/standalone-start.js +43 -0
- package/start-http.js +45 -0
- package/tools/ai_debug_investigate.json +30 -0
- package/tools/benchmark_run.json +37 -0
- package/tools/browser_a11y_check.json +21 -0
- package/tools/browser_artifacts.json +8 -0
- package/tools/browser_artifacts_clear.json +11 -0
- package/tools/browser_assert.json +16 -0
- package/tools/browser_batch.json +61 -0
- package/tools/browser_click.json +11 -0
- package/tools/browser_console.json +26 -0
- package/tools/browser_cookies.json +38 -0
- package/tools/browser_debug_report.json +11 -0
- package/tools/browser_diagnose.json +23 -0
- package/tools/browser_dom.json +11 -0
- package/tools/browser_element_status.json +26 -0
- package/tools/browser_errors.json +17 -0
- package/tools/browser_errors_aggregate.json +12 -0
- package/tools/browser_errors_clear.json +8 -0
- package/tools/browser_eval.json +11 -0
- package/tools/browser_events.json +15 -0
- package/tools/browser_events_clear.json +8 -0
- package/tools/browser_find_element.json +30 -0
- package/tools/browser_find_page.json +22 -0
- package/tools/browser_flow.json +38 -0
- package/tools/browser_har_export.json +17 -0
- package/tools/browser_highlight.json +18 -0
- package/tools/browser_hover.json +14 -0
- package/tools/browser_instrument.json +10 -0
- package/tools/browser_links.json +21 -0
- package/tools/browser_locator_suggest.json +16 -0
- package/tools/browser_locator_validate.json +12 -0
- package/tools/browser_network.json +16 -0
- package/tools/browser_network_detail.json +17 -0
- package/tools/browser_open.json +12 -0
- package/tools/browser_performance_check.json +25 -0
- package/tools/browser_press_key.json +18 -0
- package/tools/browser_quick_fix.json +29 -0
- package/tools/browser_screenshot.json +15 -0
- package/tools/browser_scroll.json +31 -0
- package/tools/browser_select.json +26 -0
- package/tools/browser_session_close.json +12 -0
- package/tools/browser_session_create.json +17 -0
- package/tools/browser_session_switch.json +12 -0
- package/tools/browser_sessions.json +8 -0
- package/tools/browser_snapshot.json +8 -0
- package/tools/browser_step.json +18 -0
- package/tools/browser_storage.json +10 -0
- package/tools/browser_trace_start.json +14 -0
- package/tools/browser_trace_stop.json +10 -0
- package/tools/browser_traverse_menu.json +25 -0
- package/tools/browser_type.json +12 -0
- package/tools/browser_verify_fix.json +39 -0
- package/tools/browser_visual_baseline.json +19 -0
- package/tools/browser_visual_compare.json +20 -0
- package/tools/browser_visual_report.json +8 -0
- package/tools/browser_wait.json +18 -0
- package/tools/debug_investigate.json +17 -0
- package/tools/error_fix_suggestion.json +13 -0
- package/tools/error_summary_md.json +11 -0
- package/tools/fix_verify.json +13 -0
- package/tools/mcp_health_check.json +8 -0
- package/tools/mcp_self_test.json +12 -0
- package/tools/screenshot_diff.json +16 -0
- package/tools/validation_check.json +20 -0
- package/tools/validation_decision.json +24 -0
- package/tools/validation_element.json +13 -0
- package/tools/validation_flow.json +12 -0
- package/tools/validation_matrix.json +27 -0
- package/tools/validation_quick_run.json +13 -0
- package/tools/validation_report.json +10 -0
- package/tools/validation_report_export.json +8 -0
- package/tools/validation_run.json +35 -0
- package/tools/validation_start.json +12 -0
- package/tools/validation_suite_run.json +17 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { loadConfig } = require('./config');
|
|
6
|
+
|
|
7
|
+
function ensureDir(dirPath = loadConfig().artifactDir) {
|
|
8
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
9
|
+
return dirPath;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function listArtifacts(rootDir = loadConfig().artifactDir) {
|
|
13
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (const name of fs.readdirSync(rootDir)) {
|
|
16
|
+
const fullPath = path.join(rootDir, name);
|
|
17
|
+
const stat = fs.statSync(fullPath);
|
|
18
|
+
if (stat.isDirectory()) {
|
|
19
|
+
entries.push(...listArtifacts(fullPath));
|
|
20
|
+
} else {
|
|
21
|
+
entries.push({ path: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanupOld(rootDir = loadConfig().artifactDir, retentionDays = 7) {
|
|
28
|
+
if (!fs.existsSync(rootDir)) return { removed: 0, files: [] };
|
|
29
|
+
const cutoff = Date.now() - Number(retentionDays) * 24 * 60 * 60 * 1000;
|
|
30
|
+
const removed = [];
|
|
31
|
+
for (const artifact of listArtifacts(rootDir)) {
|
|
32
|
+
if (artifact.mtimeMs < cutoff) {
|
|
33
|
+
fs.unlinkSync(artifact.path);
|
|
34
|
+
removed.push(artifact.path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { removed: removed.length, files: removed };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
ensureDir,
|
|
42
|
+
listArtifacts,
|
|
43
|
+
cleanupOld
|
|
44
|
+
};
|
package/core/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
artifactDir: path.join(__dirname, '..', 'artifacts'),
|
|
7
|
+
redaction: true,
|
|
8
|
+
allowlist: ['localhost', '127.0.0.1', '::1'],
|
|
9
|
+
blockedHosts: [],
|
|
10
|
+
headless: true
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function parseList(value) {
|
|
14
|
+
if (!value) return [];
|
|
15
|
+
return String(value).split(',').map(item => item.trim()).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseBool(value, fallback) {
|
|
19
|
+
if (value === undefined) return fallback;
|
|
20
|
+
return !/^(false|0|no)$/i.test(String(value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadConfig(overrides = {}) {
|
|
24
|
+
const envConfig = {
|
|
25
|
+
artifactDir: process.env.VALIDPILOT_ARTIFACT_DIR || DEFAULT_CONFIG.artifactDir,
|
|
26
|
+
redaction: parseBool(process.env.VALIDPILOT_REDACTION, DEFAULT_CONFIG.redaction),
|
|
27
|
+
allowlist: parseList(process.env.VALIDPILOT_ALLOWLIST).length ? parseList(process.env.VALIDPILOT_ALLOWLIST) : DEFAULT_CONFIG.allowlist,
|
|
28
|
+
blockedHosts: parseList(process.env.VALIDPILOT_BLOCKED_HOSTS),
|
|
29
|
+
headless: parseBool(process.env.VALIDPILOT_HEADLESS, DEFAULT_CONFIG.headless)
|
|
30
|
+
};
|
|
31
|
+
return { ...DEFAULT_CONFIG, ...envConfig, ...overrides };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
DEFAULT_CONFIG,
|
|
36
|
+
loadConfig
|
|
37
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_KEY_RE = /(password|passwd|pwd|token|secret|authorization|cookie|apikey|api_key|api-key|key)$/i;
|
|
4
|
+
const SENSITIVE_TEXT_PATTERNS = [
|
|
5
|
+
/Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
|
|
6
|
+
/ark-[A-Za-z0-9-]{20,}/gi,
|
|
7
|
+
/(api[_-]?key\s*[:=]\s*)[A-Za-z0-9._~+\/-]{8,}/gi,
|
|
8
|
+
/(token\s*[:=]\s*)[A-Za-z0-9._~+\/-]{8,}/gi
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function redactString(value) {
|
|
12
|
+
let text = String(value ?? '');
|
|
13
|
+
for (const pattern of SENSITIVE_TEXT_PATTERNS) {
|
|
14
|
+
text = text.replace(pattern, match => {
|
|
15
|
+
const prefix = match.match(/^(api[_-]?key\s*[:=]\s*|token\s*[:=]\s*)/i)?.[0] || '';
|
|
16
|
+
return `${prefix}******`;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSensitiveKey(key = '') {
|
|
23
|
+
return SENSITIVE_KEY_RE.test(String(key));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function redact(value, key = '') {
|
|
27
|
+
if (value == null) return value;
|
|
28
|
+
if (isSensitiveKey(key)) return '******';
|
|
29
|
+
if (typeof value === 'string') return redactString(value);
|
|
30
|
+
if (typeof value !== 'object') return value;
|
|
31
|
+
if (Array.isArray(value)) return value.map(item => redact(item));
|
|
32
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redact(v, k)]));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
redactString,
|
|
37
|
+
isSensitiveKey,
|
|
38
|
+
redact
|
|
39
|
+
};
|
package/core/report.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { redact } = require('./redaction');
|
|
4
|
+
|
|
5
|
+
function buildJsonReport(data = {}) {
|
|
6
|
+
return {
|
|
7
|
+
generatedAt: new Date().toISOString(),
|
|
8
|
+
ok: data.ok ?? data.pass ?? data.passed ?? false,
|
|
9
|
+
passed: data.passed ?? data.pass ?? data.ok ?? false,
|
|
10
|
+
summary: data.summary || '',
|
|
11
|
+
data: redact(data.data || data),
|
|
12
|
+
artifacts: Array.isArray(data.artifacts) ? data.artifacts : [],
|
|
13
|
+
errors: Array.isArray(data.errors) ? data.errors : []
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeHtml(value) {
|
|
18
|
+
return String(value ?? '')
|
|
19
|
+
.replace(/&/g, '&')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
.replace(/"/g, '"');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildHtmlReport(data = {}) {
|
|
26
|
+
const report = buildJsonReport(data);
|
|
27
|
+
return `<!doctype html>
|
|
28
|
+
<html lang="zh-CN">
|
|
29
|
+
<head><meta charset="utf-8"><title>ValidPilot Report</title></head>
|
|
30
|
+
<body>
|
|
31
|
+
<h1>ValidPilot Report</h1>
|
|
32
|
+
<p>Status: ${report.passed ? 'pass' : 'fail'}</p>
|
|
33
|
+
<p>${escapeHtml(report.summary)}</p>
|
|
34
|
+
<pre>${escapeHtml(JSON.stringify(report, null, 2))}</pre>
|
|
35
|
+
</body>
|
|
36
|
+
</html>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
buildJsonReport,
|
|
41
|
+
buildHtmlReport
|
|
42
|
+
};
|
package/core/result.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function buildResult(options = {}) {
|
|
4
|
+
const ok = options.ok ?? options.passed ?? false;
|
|
5
|
+
const passed = options.passed ?? ok;
|
|
6
|
+
return {
|
|
7
|
+
ok: Boolean(ok),
|
|
8
|
+
passed: Boolean(passed),
|
|
9
|
+
summary: options.summary || (passed ? 'pass' : 'fail'),
|
|
10
|
+
data: options.data || null,
|
|
11
|
+
artifacts: Array.isArray(options.artifacts) ? options.artifacts : [],
|
|
12
|
+
errors: Array.isArray(options.errors) ? options.errors : []
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pass(summary = 'pass', data = null, artifacts = []) {
|
|
17
|
+
return buildResult({ ok: true, passed: true, summary, data, artifacts });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fail(summary = 'fail', errors = [], data = null, artifacts = []) {
|
|
21
|
+
return buildResult({ ok: false, passed: false, summary, errors: Array.isArray(errors) ? errors : [errors], data, artifacts });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
buildResult,
|
|
26
|
+
result: buildResult,
|
|
27
|
+
pass,
|
|
28
|
+
fail
|
|
29
|
+
};
|
package/core/security.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadConfig } = require('./config');
|
|
4
|
+
|
|
5
|
+
function hostMatches(hostname, pattern) {
|
|
6
|
+
const host = String(hostname || '').toLowerCase();
|
|
7
|
+
const rule = String(pattern || '').toLowerCase();
|
|
8
|
+
if (!rule) return false;
|
|
9
|
+
return host === rule || host.endsWith(`.${rule}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function checkUrl(url, config = loadConfig()) {
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(url);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return { allowed: false, reason: `invalid url: ${error.message}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!['http:', 'https:', 'file:'].includes(parsed.protocol)) {
|
|
21
|
+
return { allowed: false, reason: `unsupported protocol: ${parsed.protocol}` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (parsed.protocol === 'file:') {
|
|
25
|
+
return { allowed: true, reason: 'file protocol allowed' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hostname = parsed.hostname;
|
|
29
|
+
if ((config.blockedHosts || []).some(host => hostMatches(hostname, host))) {
|
|
30
|
+
return { allowed: false, reason: `blocked host: ${hostname}` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const allowlist = config.allowlist || [];
|
|
34
|
+
if (allowlist.length && !allowlist.some(host => hostMatches(hostname, host))) {
|
|
35
|
+
return { allowed: false, reason: `host not in allowlist: ${hostname}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { allowed: true, reason: 'allowed' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeUrlLog(url) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = new URL(url);
|
|
44
|
+
parsed.username = '';
|
|
45
|
+
parsed.password = '';
|
|
46
|
+
parsed.search = parsed.search ? '?…' : '';
|
|
47
|
+
parsed.hash = '';
|
|
48
|
+
return parsed.toString();
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return String(url || '').slice(0, 200);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
checkUrl,
|
|
56
|
+
safeUrlLog
|
|
57
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
class ChromeMCPAdapter {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.browser = null;
|
|
10
|
+
this.pages = new Map();
|
|
11
|
+
this.defaultPage = null;
|
|
12
|
+
this._engine = null; // 'puppeteer' or 'playwright'
|
|
13
|
+
this.options = {
|
|
14
|
+
headless: options.headless !== false,
|
|
15
|
+
executablePath: options.executablePath || null,
|
|
16
|
+
cdpPort: options.cdpPort || 9222,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async launch(options = {}) {
|
|
22
|
+
const mergedOptions = { ...this.options, ...options };
|
|
23
|
+
|
|
24
|
+
// Try puppeteer-core first
|
|
25
|
+
try {
|
|
26
|
+
const puppeteer = require('puppeteer-core');
|
|
27
|
+
this.browser = await puppeteer.launch({
|
|
28
|
+
headless: mergedOptions.headless !== false ? 'new' : false,
|
|
29
|
+
executablePath: mergedOptions.executablePath || undefined,
|
|
30
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
31
|
+
});
|
|
32
|
+
this._engine = 'puppeteer';
|
|
33
|
+
return this.browser;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Fall through to Playwright approach
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fall back to Playwright Chromium (already available as a dependency)
|
|
39
|
+
try {
|
|
40
|
+
const { chromium } = require('playwright');
|
|
41
|
+
this.browser = await chromium.launch({
|
|
42
|
+
headless: mergedOptions.headless !== false,
|
|
43
|
+
executablePath: mergedOptions.executablePath || undefined
|
|
44
|
+
});
|
|
45
|
+
this._engine = 'playwright';
|
|
46
|
+
return this.browser;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`ChromeMCPAdapter: Cannot launch browser. ` +
|
|
50
|
+
`Install puppeteer-core or ensure Playwright is available. Error: ${e.message}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async newPage(options = {}) {
|
|
56
|
+
if (!this.browser) {
|
|
57
|
+
throw new Error('ChromeMCPAdapter: Browser not launched. Call launch() first.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let page;
|
|
61
|
+
|
|
62
|
+
if (this._engine === 'puppeteer') {
|
|
63
|
+
page = await this.browser.newPage();
|
|
64
|
+
if (options.viewport) {
|
|
65
|
+
await page.setViewport(options.viewport);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Playwright
|
|
69
|
+
page = await this.browser.newPage({
|
|
70
|
+
viewport: options.viewport || { width: 1440, height: 900 }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate a unique page name if none provided
|
|
75
|
+
const pageName = options.name || `page_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
+
this.pages.set(pageName, { page, name: pageName, createdAt: new Date().toISOString() });
|
|
77
|
+
|
|
78
|
+
if (!this.defaultPage) {
|
|
79
|
+
this.defaultPage = page;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return page;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async close() {
|
|
86
|
+
// Close all pages
|
|
87
|
+
for (const [name, entry] of this.pages) {
|
|
88
|
+
try {
|
|
89
|
+
if (entry.page && typeof entry.page.close === 'function') {
|
|
90
|
+
await entry.page.close();
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Ignore errors closing individual pages
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.pages.clear();
|
|
97
|
+
this.defaultPage = null;
|
|
98
|
+
|
|
99
|
+
// Close browser
|
|
100
|
+
if (this.browser && typeof this.browser.close === 'function') {
|
|
101
|
+
try {
|
|
102
|
+
await this.browser.close();
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Ignore errors closing browser
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.browser = null;
|
|
108
|
+
this._engine = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getPage(name = null) {
|
|
112
|
+
if (!this.browser) {
|
|
113
|
+
throw new Error('ChromeMCPAdapter: Browser not launched. Call launch() first.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (name === null) {
|
|
117
|
+
if (!this.defaultPage) {
|
|
118
|
+
throw new Error('ChromeMCPAdapter: No default page. Call newPage() first.');
|
|
119
|
+
}
|
|
120
|
+
return this.defaultPage;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const entry = this.pages.get(name);
|
|
124
|
+
if (!entry || !entry.page) {
|
|
125
|
+
throw new Error(`ChromeMCPAdapter: Page '${name}' not found.`);
|
|
126
|
+
}
|
|
127
|
+
return entry.page;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async goto(url, options = {}) {
|
|
131
|
+
const page = this.getPage();
|
|
132
|
+
const waitUntil = options.waitUntil || 'domcontentloaded';
|
|
133
|
+
|
|
134
|
+
if (this._engine === 'puppeteer') {
|
|
135
|
+
await page.goto(url, { waitUntil, timeout: options.timeout || 30000 });
|
|
136
|
+
} else {
|
|
137
|
+
await page.goto(url, { waitUntil, timeout: options.timeout || 30000 });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return page;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async screenshot(options = {}) {
|
|
144
|
+
const page = this.getPage();
|
|
145
|
+
const screenshotOptions = { fullPage: options.fullPage !== false, type: options.type || 'png' };
|
|
146
|
+
|
|
147
|
+
if (options.path) {
|
|
148
|
+
screenshotOptions.path = options.path;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this._engine === 'puppeteer') {
|
|
152
|
+
return await page.screenshot(screenshotOptions);
|
|
153
|
+
} else {
|
|
154
|
+
// Playwright returns Buffer
|
|
155
|
+
return await page.screenshot(screenshotOptions);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async evaluate(pageFunction, ...args) {
|
|
160
|
+
const page = this.getPage();
|
|
161
|
+
|
|
162
|
+
if (this._engine === 'puppeteer') {
|
|
163
|
+
return await page.evaluate(pageFunction, ...args);
|
|
164
|
+
} else {
|
|
165
|
+
return await page.evaluate(pageFunction, ...args);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async waitForSelector(selector, options = {}) {
|
|
170
|
+
const page = this.getPage();
|
|
171
|
+
const timeout = options.timeout || 10000;
|
|
172
|
+
|
|
173
|
+
if (this._engine === 'puppeteer') {
|
|
174
|
+
return await page.waitForSelector(selector, { timeout, visible: options.visible !== false });
|
|
175
|
+
} else {
|
|
176
|
+
return await page.waitForSelector(selector, {
|
|
177
|
+
timeout,
|
|
178
|
+
state: options.visible !== false ? 'visible' : 'attached'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async click(selector, options = {}) {
|
|
184
|
+
const page = this.getPage();
|
|
185
|
+
|
|
186
|
+
if (this._engine === 'puppeteer') {
|
|
187
|
+
await page.waitForSelector(selector, { timeout: options.timeout || 10000 });
|
|
188
|
+
return await page.click(selector);
|
|
189
|
+
} else {
|
|
190
|
+
await page.waitForSelector(selector, {
|
|
191
|
+
timeout: options.timeout || 10000,
|
|
192
|
+
state: 'visible'
|
|
193
|
+
});
|
|
194
|
+
return await page.click(selector);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async type(selector, text, options = {}) {
|
|
199
|
+
const page = this.getPage();
|
|
200
|
+
|
|
201
|
+
if (this._engine === 'puppeteer') {
|
|
202
|
+
await page.waitForSelector(selector, { timeout: options.timeout || 10000 });
|
|
203
|
+
await page.click(selector, { clickCount: 3 }); // Select all existing text
|
|
204
|
+
return await page.type(selector, text, { delay: options.delay || 0 });
|
|
205
|
+
} else {
|
|
206
|
+
await page.waitForSelector(selector, {
|
|
207
|
+
timeout: options.timeout || 10000,
|
|
208
|
+
state: 'visible'
|
|
209
|
+
});
|
|
210
|
+
await page.click(selector, { clickCount: 3 }); // Select all existing text
|
|
211
|
+
return await page.type(selector, text, { delay: options.delay || 0 });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isConnected() {
|
|
216
|
+
if (!this.browser) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Playwright: browser.isConnected()
|
|
222
|
+
if (typeof this.browser.isConnected === 'function') {
|
|
223
|
+
return this.browser.isConnected();
|
|
224
|
+
}
|
|
225
|
+
// Puppeteer: browser.isConnected()
|
|
226
|
+
if (typeof this.browser.isConnected === 'function') {
|
|
227
|
+
return this.browser.isConnected();
|
|
228
|
+
}
|
|
229
|
+
// Fallback: if browser exists and no error thrown, assume connected
|
|
230
|
+
return true;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static detectChromePath() {
|
|
237
|
+
const platform = os.platform();
|
|
238
|
+
|
|
239
|
+
if (platform === 'win32') {
|
|
240
|
+
const winPaths = [
|
|
241
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
242
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
243
|
+
path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
244
|
+
'C:\\Program Files\\Chromium\\Application\\chrome.exe'
|
|
245
|
+
];
|
|
246
|
+
for (const p of winPaths) {
|
|
247
|
+
if (fs.existsSync(p)) {
|
|
248
|
+
return p;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Try registry lookup
|
|
253
|
+
try {
|
|
254
|
+
const { execSync } = require('child_process');
|
|
255
|
+
const regQuery = execSync(
|
|
256
|
+
'reg query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe" /ve 2>nul',
|
|
257
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
258
|
+
);
|
|
259
|
+
const match = regQuery.match(/([A-Z]:\\[^\r\n]+\.exe)/i);
|
|
260
|
+
if (match && fs.existsSync(match[1])) {
|
|
261
|
+
return match[1];
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// Registry query failed
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (platform === 'darwin') {
|
|
269
|
+
const macPaths = [
|
|
270
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
271
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
272
|
+
];
|
|
273
|
+
for (const p of macPaths) {
|
|
274
|
+
if (fs.existsSync(p)) {
|
|
275
|
+
return p;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (platform === 'linux') {
|
|
281
|
+
const linuxPaths = [
|
|
282
|
+
'/usr/bin/google-chrome',
|
|
283
|
+
'/usr/bin/chromium-browser',
|
|
284
|
+
'/usr/bin/chromium',
|
|
285
|
+
'/snap/bin/chromium'
|
|
286
|
+
];
|
|
287
|
+
for (const p of linuxPaths) {
|
|
288
|
+
if (fs.existsSync(p)) {
|
|
289
|
+
return p;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Try which command
|
|
294
|
+
try {
|
|
295
|
+
const { execSync } = require('child_process');
|
|
296
|
+
const names = ['google-chrome', 'chromium-browser', 'chromium'];
|
|
297
|
+
for (const name of names) {
|
|
298
|
+
try {
|
|
299
|
+
const result = execSync(`which ${name} 2>/dev/null`, {
|
|
300
|
+
encoding: 'utf8',
|
|
301
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
302
|
+
}).trim();
|
|
303
|
+
if (result && fs.existsSync(result)) {
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// Command not found
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// which command failed
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { ChromeMCPAdapter };
|