@serjm/deepseek-code 0.3.1
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/CONTRIBUTING.md +73 -0
- package/README.md +194 -0
- package/README.ru.md +194 -0
- package/dist/api/index.d.ts +77 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +263 -0
- package/dist/api/index.js.map +1 -0
- package/dist/cli/headless.d.ts +22 -0
- package/dist/cli/headless.d.ts.map +1 -0
- package/dist/cli/headless.js +122 -0
- package/dist/cli/headless.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +90 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/interactive.d.ts +18 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +75 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/commands/index.d.ts +30 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +964 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/config/defaults.d.ts +37 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +39 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +76 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/core/agent-loop.d.ts +111 -0
- package/dist/core/agent-loop.d.ts.map +1 -0
- package/dist/core/agent-loop.js +485 -0
- package/dist/core/agent-loop.js.map +1 -0
- package/dist/core/checkpoint.d.ts +10 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +83 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/extensions.d.ts +55 -0
- package/dist/core/extensions.d.ts.map +1 -0
- package/dist/core/extensions.js +113 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/git.d.ts +68 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +148 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/hooks.d.ts +37 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +77 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/i18n.d.ts +90 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +253 -0
- package/dist/core/i18n.js.map +1 -0
- package/dist/core/lsp.d.ts +74 -0
- package/dist/core/lsp.d.ts.map +1 -0
- package/dist/core/lsp.js +239 -0
- package/dist/core/lsp.js.map +1 -0
- package/dist/core/mcp.d.ts +49 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +195 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/memory.d.ts +38 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +231 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/metrics.d.ts +36 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +111 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/review.d.ts +27 -0
- package/dist/core/review.d.ts.map +1 -0
- package/dist/core/review.js +201 -0
- package/dist/core/review.js.map +1 -0
- package/dist/core/sandbox.d.ts +52 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandbox.js +140 -0
- package/dist/core/sandbox.js.map +1 -0
- package/dist/core/scheduler.d.ts +56 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +167 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/session.d.ts +49 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +127 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/skills.d.ts +36 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +90 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/subagent.d.ts +45 -0
- package/dist/core/subagent.d.ts.map +1 -0
- package/dist/core/subagent.js +130 -0
- package/dist/core/subagent.js.map +1 -0
- package/dist/core/themes.d.ts +35 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/themes.js +188 -0
- package/dist/core/themes.js.map +1 -0
- package/dist/tools/bash.d.ts +3 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +92 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/chrome-manager.d.ts +35 -0
- package/dist/tools/chrome-manager.d.ts.map +1 -0
- package/dist/tools/chrome-manager.js +163 -0
- package/dist/tools/chrome-manager.js.map +1 -0
- package/dist/tools/chrome.d.ts +78 -0
- package/dist/tools/chrome.d.ts.map +1 -0
- package/dist/tools/chrome.js +1058 -0
- package/dist/tools/chrome.js.map +1 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +81 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +3 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +41 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +74 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/path-safety.d.ts +3 -0
- package/dist/tools/path-safety.d.ts.map +1 -0
- package/dist/tools/path-safety.js +19 -0
- package/dist/tools/path-safety.js.map +1 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +58 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +43 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +47 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +90 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +51 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/ui/activity-cards.d.ts +50 -0
- package/dist/ui/activity-cards.d.ts.map +1 -0
- package/dist/ui/activity-cards.js +185 -0
- package/dist/ui/activity-cards.js.map +1 -0
- package/dist/ui/app.d.ts +9 -0
- package/dist/ui/app.d.ts.map +1 -0
- package/dist/ui/app.js +852 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/chat-view.d.ts +10 -0
- package/dist/ui/chat-view.d.ts.map +1 -0
- package/dist/ui/chat-view.js +94 -0
- package/dist/ui/chat-view.js.map +1 -0
- package/dist/ui/error-boundary.d.ts +13 -0
- package/dist/ui/error-boundary.d.ts.map +1 -0
- package/dist/ui/error-boundary.js +16 -0
- package/dist/ui/error-boundary.js.map +1 -0
- package/dist/ui/fade-in.d.ts +8 -0
- package/dist/ui/fade-in.d.ts.map +1 -0
- package/dist/ui/fade-in.js +14 -0
- package/dist/ui/fade-in.js.map +1 -0
- package/dist/ui/input-bar.d.ts +16 -0
- package/dist/ui/input-bar.d.ts.map +1 -0
- package/dist/ui/input-bar.js +269 -0
- package/dist/ui/input-bar.js.map +1 -0
- package/dist/ui/markdown-view.d.ts +9 -0
- package/dist/ui/markdown-view.d.ts.map +1 -0
- package/dist/ui/markdown-view.js +240 -0
- package/dist/ui/markdown-view.js.map +1 -0
- package/dist/ui/matrix-rain.d.ts +2 -0
- package/dist/ui/matrix-rain.d.ts.map +1 -0
- package/dist/ui/matrix-rain.js +134 -0
- package/dist/ui/matrix-rain.js.map +1 -0
- package/dist/ui/reasoning-view.d.ts +12 -0
- package/dist/ui/reasoning-view.d.ts.map +1 -0
- package/dist/ui/reasoning-view.js +34 -0
- package/dist/ui/reasoning-view.js.map +1 -0
- package/dist/ui/results-panel.d.ts +11 -0
- package/dist/ui/results-panel.d.ts.map +1 -0
- package/dist/ui/results-panel.js +17 -0
- package/dist/ui/results-panel.js.map +1 -0
- package/dist/ui/setup-wizard.d.ts +30 -0
- package/dist/ui/setup-wizard.d.ts.map +1 -0
- package/dist/ui/setup-wizard.js +166 -0
- package/dist/ui/setup-wizard.js.map +1 -0
- package/dist/ui/status-bar.d.ts +14 -0
- package/dist/ui/status-bar.d.ts.map +1 -0
- package/dist/ui/status-bar.js +63 -0
- package/dist/ui/status-bar.js.map +1 -0
- package/dist/ui/tool-activity-card.d.ts +9 -0
- package/dist/ui/tool-activity-card.d.ts.map +1 -0
- package/dist/ui/tool-activity-card.js +172 -0
- package/dist/ui/tool-activity-card.js.map +1 -0
- package/dist/ui/tool-call-view.d.ts +9 -0
- package/dist/ui/tool-call-view.d.ts.map +1 -0
- package/dist/ui/tool-call-view.js +149 -0
- package/dist/ui/tool-call-view.js.map +1 -0
- package/dist/utils/clipboard.d.ts +6 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js +56 -0
- package/dist/utils/clipboard.js.map +1 -0
- package/dist/utils/ignore.d.ts +6 -0
- package/dist/utils/ignore.d.ts.map +1 -0
- package/dist/utils/ignore.js +40 -0
- package/dist/utils/ignore.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +13 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/string-width.d.ts +6 -0
- package/dist/utils/string-width.d.ts.map +1 -0
- package/dist/utils/string-width.js +37 -0
- package/dist/utils/string-width.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { chromeManager } from './chrome-manager.js';
|
|
5
|
+
const CHROME_ACTIONS = [
|
|
6
|
+
'open',
|
|
7
|
+
'click',
|
|
8
|
+
'fill',
|
|
9
|
+
'eval',
|
|
10
|
+
'text',
|
|
11
|
+
'html',
|
|
12
|
+
'console',
|
|
13
|
+
'network',
|
|
14
|
+
'state',
|
|
15
|
+
'shot',
|
|
16
|
+
'nav',
|
|
17
|
+
'wait',
|
|
18
|
+
'scroll',
|
|
19
|
+
'locator',
|
|
20
|
+
'cookies',
|
|
21
|
+
'storage',
|
|
22
|
+
'quiz',
|
|
23
|
+
];
|
|
24
|
+
function isChromeAction(action) {
|
|
25
|
+
return typeof action === 'string' && CHROME_ACTIONS.includes(action);
|
|
26
|
+
}
|
|
27
|
+
async function waitForElement(page, selector, timeout = 10000) {
|
|
28
|
+
await page.waitForSelector(selector, {
|
|
29
|
+
visible: true,
|
|
30
|
+
timeout,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function formatEvalResult(result) {
|
|
34
|
+
if (result === null || result === undefined)
|
|
35
|
+
return 'undefined';
|
|
36
|
+
if (typeof result === 'string')
|
|
37
|
+
return result;
|
|
38
|
+
if (typeof result === 'object') {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.stringify(result, null, 2);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return String(result);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return String(result);
|
|
47
|
+
}
|
|
48
|
+
function sleep(ms) {
|
|
49
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
async function resolveArtifactPath(output) {
|
|
52
|
+
const artifactDir = join(process.cwd(), '.deepseek-code', 'artifacts', 'browser');
|
|
53
|
+
const targetPath = output
|
|
54
|
+
? (isAbsolute(output) ? output : join(process.cwd(), output))
|
|
55
|
+
: join(artifactDir, `chrome-shot-${Date.now()}.png`);
|
|
56
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
57
|
+
return targetPath;
|
|
58
|
+
}
|
|
59
|
+
async function navigateIfNeeded(args, page) {
|
|
60
|
+
if (args.url) {
|
|
61
|
+
await chromeManager.navigate(args.url, args.sameTab ?? false);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (page.url() === 'about:blank') {
|
|
65
|
+
throw new Error('URL is required when no page is open yet');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function collectConsoleMessages(page, args) {
|
|
69
|
+
const logs = [];
|
|
70
|
+
const errors = [];
|
|
71
|
+
const handleConsole = (msg) => {
|
|
72
|
+
const text = msg.text();
|
|
73
|
+
if (msg.type() === 'error') {
|
|
74
|
+
errors.push(text);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
logs.push(text);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
page.on('console', handleConsole);
|
|
81
|
+
try {
|
|
82
|
+
if (args.url) {
|
|
83
|
+
await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: args.timeout ?? 30000 });
|
|
84
|
+
}
|
|
85
|
+
await sleep(2000);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
page.off('console', handleConsole);
|
|
89
|
+
}
|
|
90
|
+
if (args.error ?? false) {
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
output: errors.length > 0
|
|
94
|
+
? `Console errors:\n${errors.join('\n')}`
|
|
95
|
+
: 'No console errors found',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const parts = [];
|
|
99
|
+
if ((args.all ?? false) || logs.length > 0) {
|
|
100
|
+
parts.push(`Console logs (${logs.length}):\n${logs.join('\n')}`);
|
|
101
|
+
}
|
|
102
|
+
if (errors.length > 0) {
|
|
103
|
+
parts.push(`Console errors (${errors.length}):\n${errors.join('\n')}`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
output: parts.length > 0 ? parts.join('\n\n') : 'No console messages',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function collectNetworkRequests(page, args) {
|
|
111
|
+
const requests = [];
|
|
112
|
+
const handleRequest = (req) => {
|
|
113
|
+
requests.push({
|
|
114
|
+
url: req.url(),
|
|
115
|
+
method: req.method(),
|
|
116
|
+
type: req.resourceType(),
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
page.on('request', handleRequest);
|
|
120
|
+
try {
|
|
121
|
+
if (args.url) {
|
|
122
|
+
await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: args.timeout ?? 30000 });
|
|
123
|
+
}
|
|
124
|
+
await sleep(2000);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
page.off('request', handleRequest);
|
|
128
|
+
}
|
|
129
|
+
const filtered = (args.api ?? false)
|
|
130
|
+
? requests.filter(r => r.type === 'xhr' || r.type === 'fetch')
|
|
131
|
+
: requests;
|
|
132
|
+
if (filtered.length === 0) {
|
|
133
|
+
return { success: true, output: 'No network requests captured' };
|
|
134
|
+
}
|
|
135
|
+
const lines = filtered.map((r, i) => `${i + 1}. [${r.method}] ${r.type}: ${r.url}`);
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
output: `Network requests (${filtered.length}):\n${lines.join('\n')}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function executeAction(args) {
|
|
142
|
+
if (!isChromeAction(args.action)) {
|
|
143
|
+
return { success: false, output: '', error: `Unknown action: ${String(args.action)}` };
|
|
144
|
+
}
|
|
145
|
+
if (args.action === 'state') {
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
output: JSON.stringify(chromeManager.getState(), null, 2),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const timeout = args.timeout ?? 10000;
|
|
152
|
+
const sameTab = args.sameTab ?? true;
|
|
153
|
+
if (args.headless !== undefined) {
|
|
154
|
+
await chromeManager.ensureMode(args.headless);
|
|
155
|
+
}
|
|
156
|
+
const page = await chromeManager.getPage(sameTab);
|
|
157
|
+
try {
|
|
158
|
+
switch (args.action) {
|
|
159
|
+
case 'open': {
|
|
160
|
+
if (!args.url)
|
|
161
|
+
return { success: false, output: '', error: 'URL is required for open action' };
|
|
162
|
+
await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout });
|
|
163
|
+
return { success: true, output: `Opened: ${page.url()}` };
|
|
164
|
+
}
|
|
165
|
+
case 'click': {
|
|
166
|
+
if (!args.selector)
|
|
167
|
+
return { success: false, output: '', error: 'Selector is required for click action' };
|
|
168
|
+
await navigateIfNeeded(args, page);
|
|
169
|
+
await waitForElement(page, args.selector, timeout);
|
|
170
|
+
await page.click(args.selector);
|
|
171
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout }).catch(() => { });
|
|
172
|
+
return { success: true, output: `Clicked: ${args.selector}` };
|
|
173
|
+
}
|
|
174
|
+
case 'fill': {
|
|
175
|
+
if (!args.selector || args.text === undefined) {
|
|
176
|
+
return { success: false, output: '', error: 'Selector and text are required for fill action' };
|
|
177
|
+
}
|
|
178
|
+
await navigateIfNeeded(args, page);
|
|
179
|
+
await waitForElement(page, args.selector, timeout);
|
|
180
|
+
await page.click(args.selector);
|
|
181
|
+
await page.keyboard.down('Control');
|
|
182
|
+
await page.keyboard.press('a');
|
|
183
|
+
await page.keyboard.up('Control');
|
|
184
|
+
await page.keyboard.press('Backspace');
|
|
185
|
+
await page.type(args.selector, args.text, { delay: 10 });
|
|
186
|
+
return { success: true, output: `Filled "${args.text}" into: ${args.selector}` };
|
|
187
|
+
}
|
|
188
|
+
case 'eval': {
|
|
189
|
+
if (!args.code)
|
|
190
|
+
return { success: false, output: '', error: 'Code is required for eval action' };
|
|
191
|
+
await navigateIfNeeded(args, page);
|
|
192
|
+
const result = await page.evaluate(args.code);
|
|
193
|
+
return { success: true, output: formatEvalResult(result) };
|
|
194
|
+
}
|
|
195
|
+
case 'text': {
|
|
196
|
+
await navigateIfNeeded(args, page);
|
|
197
|
+
if (args.selector) {
|
|
198
|
+
const el = await page.$(args.selector);
|
|
199
|
+
if (!el)
|
|
200
|
+
return { success: false, output: '', error: `Element not found: ${args.selector}` };
|
|
201
|
+
const textContent = await page.evaluate(elm => elm.textContent ?? '', el);
|
|
202
|
+
return { success: true, output: textContent.trim() };
|
|
203
|
+
}
|
|
204
|
+
const textContent = await page.evaluate(() => document.body?.innerText ?? '');
|
|
205
|
+
return { success: true, output: textContent.trim() };
|
|
206
|
+
}
|
|
207
|
+
case 'html': {
|
|
208
|
+
await navigateIfNeeded(args, page);
|
|
209
|
+
return { success: true, output: await page.content() };
|
|
210
|
+
}
|
|
211
|
+
case 'console':
|
|
212
|
+
return collectConsoleMessages(page, args);
|
|
213
|
+
case 'network':
|
|
214
|
+
return collectNetworkRequests(page, args);
|
|
215
|
+
case 'shot': {
|
|
216
|
+
await navigateIfNeeded(args, page);
|
|
217
|
+
const shotPath = await resolveArtifactPath(args.output);
|
|
218
|
+
await page.screenshot({
|
|
219
|
+
path: shotPath,
|
|
220
|
+
fullPage: args.full ?? false,
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
output: `Screenshot saved: ${shotPath}\nPage: ${page.url()}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
case 'nav': {
|
|
228
|
+
if (args.back) {
|
|
229
|
+
await page.goBack({ waitUntil: 'load', timeout });
|
|
230
|
+
return { success: true, output: `Navigated back to: ${page.url()}` };
|
|
231
|
+
}
|
|
232
|
+
if (args.forward) {
|
|
233
|
+
await page.goForward({ waitUntil: 'load', timeout });
|
|
234
|
+
return { success: true, output: `Navigated forward to: ${page.url()}` };
|
|
235
|
+
}
|
|
236
|
+
if (args.refresh ?? true) {
|
|
237
|
+
await page.reload({ waitUntil: 'load', timeout });
|
|
238
|
+
return { success: true, output: `Page refreshed: ${page.url()}` };
|
|
239
|
+
}
|
|
240
|
+
return { success: true, output: `Current URL: ${page.url()}` };
|
|
241
|
+
}
|
|
242
|
+
case 'wait': {
|
|
243
|
+
if (!args.selector)
|
|
244
|
+
return { success: false, output: '', error: 'Selector is required for wait action' };
|
|
245
|
+
await navigateIfNeeded(args, page);
|
|
246
|
+
if (args.hidden ?? false) {
|
|
247
|
+
await page.waitForSelector(args.selector, { hidden: true, timeout });
|
|
248
|
+
return { success: true, output: `Element hidden: ${args.selector}` };
|
|
249
|
+
}
|
|
250
|
+
await page.waitForSelector(args.selector, { visible: true, timeout });
|
|
251
|
+
return { success: true, output: `Element visible: ${args.selector}` };
|
|
252
|
+
}
|
|
253
|
+
case 'scroll': {
|
|
254
|
+
await navigateIfNeeded(args, page);
|
|
255
|
+
if (args.top) {
|
|
256
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
257
|
+
return { success: true, output: 'Scrolled to top' };
|
|
258
|
+
}
|
|
259
|
+
if (args.selector) {
|
|
260
|
+
await page.evaluate((selector) => {
|
|
261
|
+
const el = document.querySelector(selector);
|
|
262
|
+
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
263
|
+
}, args.selector);
|
|
264
|
+
return { success: true, output: `Scrolled to element: ${args.selector}` };
|
|
265
|
+
}
|
|
266
|
+
if (args.bottom) {
|
|
267
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
268
|
+
await sleep(500);
|
|
269
|
+
return { success: true, output: 'Scrolled to bottom' };
|
|
270
|
+
}
|
|
271
|
+
return { success: true, output: 'No scroll target specified' };
|
|
272
|
+
}
|
|
273
|
+
case 'locator': {
|
|
274
|
+
if (!args.selector)
|
|
275
|
+
return { success: false, output: '', error: 'Selector is required for locator action' };
|
|
276
|
+
await navigateIfNeeded(args, page);
|
|
277
|
+
const elements = await page.$$(args.selector);
|
|
278
|
+
if (args.count ?? false) {
|
|
279
|
+
return { success: true, output: `Found ${elements.length} elements matching: ${args.selector}` };
|
|
280
|
+
}
|
|
281
|
+
const results = [];
|
|
282
|
+
for (let i = 0; i < elements.length; i++) {
|
|
283
|
+
const el = elements[i];
|
|
284
|
+
const tagName = await page.evaluate(node => node.tagName.toLowerCase(), el);
|
|
285
|
+
const elText = await page.evaluate(node => node.innerText?.slice(0, 100) ?? '', el);
|
|
286
|
+
const elAttrs = await page.evaluate(node => {
|
|
287
|
+
const attrs = {};
|
|
288
|
+
for (const attr of node.attributes) {
|
|
289
|
+
attrs[attr.name] = attr.value;
|
|
290
|
+
}
|
|
291
|
+
return attrs;
|
|
292
|
+
}, el);
|
|
293
|
+
if (args.text && !elText.toLowerCase().includes(args.text.toLowerCase()))
|
|
294
|
+
continue;
|
|
295
|
+
if (args.attr && elAttrs[args.attr] === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
const attrStr = Object.entries(elAttrs)
|
|
298
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
299
|
+
.join(' ');
|
|
300
|
+
results.push(`[${i}] <${tagName}${attrStr ? ` ${attrStr}` : ''}>${elText ? `\n Text: ${elText.trim().slice(0, 200)}` : ''}`);
|
|
301
|
+
}
|
|
302
|
+
if (results.length === 0) {
|
|
303
|
+
return { success: true, output: `No elements found matching: ${args.selector}` };
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
output: `Elements (${results.length}):\n${results.join('\n')}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case 'cookies': {
|
|
311
|
+
await navigateIfNeeded(args, page);
|
|
312
|
+
if (args.clear ?? false) {
|
|
313
|
+
const client = await page.target().createCDPSession();
|
|
314
|
+
await client.send('Network.clearBrowserCookies');
|
|
315
|
+
return { success: true, output: 'Cookies cleared' };
|
|
316
|
+
}
|
|
317
|
+
const cookies = await page.cookies();
|
|
318
|
+
if (args.name) {
|
|
319
|
+
const cookie = cookies.find(cookie => cookie.name === args.name);
|
|
320
|
+
if (!cookie)
|
|
321
|
+
return { success: false, output: '', error: `Cookie not found: ${args.name}` };
|
|
322
|
+
return { success: true, output: JSON.stringify(cookie, null, 2) };
|
|
323
|
+
}
|
|
324
|
+
if (cookies.length === 0) {
|
|
325
|
+
return { success: true, output: 'No cookies found' };
|
|
326
|
+
}
|
|
327
|
+
const lines = cookies.map(cookie => `${cookie.name}=${cookie.value} (domain: ${cookie.domain}, path: ${cookie.path})`);
|
|
328
|
+
return {
|
|
329
|
+
success: true,
|
|
330
|
+
output: `Cookies (${cookies.length}):\n${lines.join('\n')}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
case 'storage': {
|
|
334
|
+
await navigateIfNeeded(args, page);
|
|
335
|
+
const result = {};
|
|
336
|
+
const readLocal = args.local ?? !args.session;
|
|
337
|
+
if (readLocal) {
|
|
338
|
+
const localData = await page.evaluate(() => {
|
|
339
|
+
const data = {};
|
|
340
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
341
|
+
const key = localStorage.key(i);
|
|
342
|
+
if (key)
|
|
343
|
+
data[key] = localStorage.getItem(key) ?? '';
|
|
344
|
+
}
|
|
345
|
+
return data;
|
|
346
|
+
});
|
|
347
|
+
result.localStorage = JSON.stringify(localData, null, 2);
|
|
348
|
+
}
|
|
349
|
+
if (args.session) {
|
|
350
|
+
const sessionData = await page.evaluate(() => {
|
|
351
|
+
const data = {};
|
|
352
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
353
|
+
const key = sessionStorage.key(i);
|
|
354
|
+
if (key)
|
|
355
|
+
data[key] = sessionStorage.getItem(key) ?? '';
|
|
356
|
+
}
|
|
357
|
+
return data;
|
|
358
|
+
});
|
|
359
|
+
result.sessionStorage = JSON.stringify(sessionData, null, 2);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
output: Object.entries(result)
|
|
364
|
+
.map(([key, value]) => `${key}:\n${value}`)
|
|
365
|
+
.join('\n\n'),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
case 'quiz': {
|
|
369
|
+
if (!args.url)
|
|
370
|
+
return { success: false, output: '', error: 'URL is required for quiz action' };
|
|
371
|
+
await chromeManager.navigate(args.url, sameTab);
|
|
372
|
+
const strategy = args.quizStrategy ?? 'first';
|
|
373
|
+
let answered = 0;
|
|
374
|
+
const questions = await page.$$('[class*="question"], [class*="quiz"], [class*="test"]');
|
|
375
|
+
for (const question of questions) {
|
|
376
|
+
const options = await question.$$('input[type="radio"], input[type="checkbox"], [class*="option"], [class*="answer"], li, button');
|
|
377
|
+
if (options.length === 0)
|
|
378
|
+
continue;
|
|
379
|
+
let targetIndex = 0;
|
|
380
|
+
if (strategy === 'random') {
|
|
381
|
+
targetIndex = Math.floor(Math.random() * options.length);
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
await options[targetIndex].click();
|
|
385
|
+
answered++;
|
|
386
|
+
await sleep(300);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Best-effort flow for generic quizzes.
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
success: true,
|
|
394
|
+
output: `Quiz completed: ${answered} questions answered (strategy: ${strategy})`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
default:
|
|
398
|
+
return { success: false, output: '', error: `Unknown action: ${args.action}` };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
403
|
+
return { success: false, output: '', error: `Chrome ${args.action} failed: ${message}` };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const chromeParameters = [
|
|
407
|
+
{
|
|
408
|
+
name: 'action',
|
|
409
|
+
type: 'string',
|
|
410
|
+
description: 'Browser action: open, click, fill, eval, text, html, console, network, state, shot, nav, wait, scroll, locator, cookies, storage, quiz',
|
|
411
|
+
required: true,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: 'url',
|
|
415
|
+
type: 'string',
|
|
416
|
+
description: 'Target page URL for opening or navigation',
|
|
417
|
+
required: false,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: 'selector',
|
|
421
|
+
type: 'string',
|
|
422
|
+
description: 'CSS selector for click, fill, text, wait, scroll, or locator',
|
|
423
|
+
required: false,
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: 'text',
|
|
427
|
+
type: 'string',
|
|
428
|
+
description: 'Text to fill into an input or text filter for locator',
|
|
429
|
+
required: false,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'code',
|
|
433
|
+
type: 'string',
|
|
434
|
+
description: 'JavaScript expression or code to evaluate on the page',
|
|
435
|
+
required: false,
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'output',
|
|
439
|
+
type: 'string',
|
|
440
|
+
description: 'Screenshot output path for shot action',
|
|
441
|
+
required: false,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'timeout',
|
|
445
|
+
type: 'number',
|
|
446
|
+
description: 'Timeout in milliseconds for wait-like actions',
|
|
447
|
+
required: false,
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: 'sameTab',
|
|
451
|
+
type: 'boolean',
|
|
452
|
+
description: 'Reuse the current tab for multi-step browser flows',
|
|
453
|
+
required: false,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: 'hidden',
|
|
457
|
+
type: 'boolean',
|
|
458
|
+
description: 'Wait for the selector to become hidden instead of visible',
|
|
459
|
+
required: false,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: 'error',
|
|
463
|
+
type: 'boolean',
|
|
464
|
+
description: 'Show only console errors',
|
|
465
|
+
required: false,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'all',
|
|
469
|
+
type: 'boolean',
|
|
470
|
+
description: 'Show all console messages',
|
|
471
|
+
required: false,
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: 'api',
|
|
475
|
+
type: 'boolean',
|
|
476
|
+
description: 'Show only XHR/fetch network requests',
|
|
477
|
+
required: false,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: 'local',
|
|
481
|
+
type: 'boolean',
|
|
482
|
+
description: 'Read localStorage',
|
|
483
|
+
required: false,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: 'session',
|
|
487
|
+
type: 'boolean',
|
|
488
|
+
description: 'Read sessionStorage',
|
|
489
|
+
required: false,
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: 'clear',
|
|
493
|
+
type: 'boolean',
|
|
494
|
+
description: 'Clear cookies',
|
|
495
|
+
required: false,
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: 'full',
|
|
499
|
+
type: 'boolean',
|
|
500
|
+
description: 'Take a full-page screenshot',
|
|
501
|
+
required: false,
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: 'back',
|
|
505
|
+
type: 'boolean',
|
|
506
|
+
description: 'Navigate back in history',
|
|
507
|
+
required: false,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: 'forward',
|
|
511
|
+
type: 'boolean',
|
|
512
|
+
description: 'Navigate forward in history',
|
|
513
|
+
required: false,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'refresh',
|
|
517
|
+
type: 'boolean',
|
|
518
|
+
description: 'Reload the current page',
|
|
519
|
+
required: false,
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: 'top',
|
|
523
|
+
type: 'boolean',
|
|
524
|
+
description: 'Scroll to top',
|
|
525
|
+
required: false,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'bottom',
|
|
529
|
+
type: 'boolean',
|
|
530
|
+
description: 'Scroll to bottom',
|
|
531
|
+
required: false,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'name',
|
|
535
|
+
type: 'string',
|
|
536
|
+
description: 'Cookie name to inspect',
|
|
537
|
+
required: false,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'attr',
|
|
541
|
+
type: 'string',
|
|
542
|
+
description: 'Attribute filter for locator',
|
|
543
|
+
required: false,
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: 'count',
|
|
547
|
+
type: 'boolean',
|
|
548
|
+
description: 'Return only the count of matching elements',
|
|
549
|
+
required: false,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: 'port',
|
|
553
|
+
type: 'number',
|
|
554
|
+
description: 'Override Chrome remote debugging port',
|
|
555
|
+
required: false,
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: 'headless',
|
|
559
|
+
type: 'boolean',
|
|
560
|
+
description: 'Run the browser in headless mode for automation or CI',
|
|
561
|
+
required: false,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'quizStrategy',
|
|
565
|
+
type: 'string',
|
|
566
|
+
description: 'Quiz answer strategy: first or random',
|
|
567
|
+
required: false,
|
|
568
|
+
},
|
|
569
|
+
];
|
|
570
|
+
let lastBrowserTestResult = null;
|
|
571
|
+
export function getLastBrowserTestResult() {
|
|
572
|
+
return lastBrowserTestResult;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Start a temporary HTTP server serving the browser test page.
|
|
576
|
+
* Returns the server and the URL.
|
|
577
|
+
*/
|
|
578
|
+
async function startTestServer() {
|
|
579
|
+
const html = `<!DOCTYPE html>
|
|
580
|
+
<html lang="en">
|
|
581
|
+
<head><meta charset="UTF-8"><title>Browser Test Page</title></head>
|
|
582
|
+
<body>
|
|
583
|
+
<h1 id="test-title">Browser Test</h1>
|
|
584
|
+
<p id="test-paragraph">Hello, world!</p>
|
|
585
|
+
<button id="test-button" onclick="document.getElementById('test-output').textContent='clicked'">Click Me</button>
|
|
586
|
+
<input id="test-input" type="text" placeholder="Type here">
|
|
587
|
+
<label><input type="checkbox" id="test-checkbox" checked> Checkbox</label>
|
|
588
|
+
<label><input type="radio" name="test-radio" id="radio-1" checked> Radio 1</label>
|
|
589
|
+
<label><input type="radio" name="test-radio" id="radio-2"> Radio 2</label>
|
|
590
|
+
<div id="test-output"></div>
|
|
591
|
+
<script>
|
|
592
|
+
console.log('Browser test page loaded');
|
|
593
|
+
console.error('Test error message');
|
|
594
|
+
localStorage.setItem('test-key', 'test-value');
|
|
595
|
+
sessionStorage.setItem('session-key', 'session-value');
|
|
596
|
+
document.cookie = 'test-cookie=chocolate-chip; path=/';
|
|
597
|
+
</script>
|
|
598
|
+
</body>
|
|
599
|
+
</html>`;
|
|
600
|
+
return new Promise((resolve, reject) => {
|
|
601
|
+
const server = createServer((_req, res) => {
|
|
602
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
603
|
+
res.end(html);
|
|
604
|
+
});
|
|
605
|
+
server.listen(0, '127.0.0.1', () => {
|
|
606
|
+
const addr = server.address();
|
|
607
|
+
if (!addr || typeof addr === 'string') {
|
|
608
|
+
server.close();
|
|
609
|
+
reject(new Error('Failed to get server address'));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
resolve({ server, url: `http://127.0.0.1:${addr.port}` });
|
|
613
|
+
});
|
|
614
|
+
server.unref();
|
|
615
|
+
server.on('error', reject);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Run a controlled browser test on a local HTTP page.
|
|
620
|
+
*
|
|
621
|
+
* @param options - Optional: headless mode, abort signal
|
|
622
|
+
* @returns Markdown report string
|
|
623
|
+
*
|
|
624
|
+
* Features:
|
|
625
|
+
* - All steps use sameTab: true on a single page via local HTTP server
|
|
626
|
+
* - Each step has a timeout (30s max)
|
|
627
|
+
* - A single step failure does NOT abort the entire test
|
|
628
|
+
* - Supports --headless and --headed modes
|
|
629
|
+
* - Supports abort via AbortSignal
|
|
630
|
+
* - Always produces a final structured report
|
|
631
|
+
* - Saves structured result for /last-browser-test
|
|
632
|
+
*/
|
|
633
|
+
export async function browserTest(options) {
|
|
634
|
+
const desiredHeadless = options?.headless ?? false; // default: headed (visible)
|
|
635
|
+
const signal = options?.signal;
|
|
636
|
+
const requestedMode = desiredHeadless ? 'headless' : 'headed';
|
|
637
|
+
const steps = [];
|
|
638
|
+
// Ensure browser is in the correct mode — closes and re-launches if needed
|
|
639
|
+
let initFailed = false;
|
|
640
|
+
let actualHeadless = desiredHeadless;
|
|
641
|
+
try {
|
|
642
|
+
await chromeManager.ensureMode(desiredHeadless);
|
|
643
|
+
const state = chromeManager.getState();
|
|
644
|
+
actualHeadless = state.headless;
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
// If we can't even ensure the mode, add a step for it
|
|
648
|
+
initFailed = true;
|
|
649
|
+
steps.push({
|
|
650
|
+
name: 'browser init',
|
|
651
|
+
status: 'failed',
|
|
652
|
+
durationMs: 0,
|
|
653
|
+
details: `Не удалось запустить Chrome: ${String(err)}. Проверьте, не занят ли порт 9222 другим процессом Chrome.`,
|
|
654
|
+
});
|
|
655
|
+
return buildBrowserTestReport(steps, requestedMode, actualHeadless, signal?.aborted ?? false, undefined, initFailed);
|
|
656
|
+
}
|
|
657
|
+
// Start temporary HTTP server
|
|
658
|
+
let server = null;
|
|
659
|
+
let testUrl = null;
|
|
660
|
+
let serverError = null;
|
|
661
|
+
try {
|
|
662
|
+
const result = await startTestServer();
|
|
663
|
+
server = result.server;
|
|
664
|
+
testUrl = result.url;
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
serverError = String(err);
|
|
668
|
+
}
|
|
669
|
+
if (!testUrl) {
|
|
670
|
+
// Fall back to data: URL for basic tests, skip storage/network
|
|
671
|
+
const dataUrl = `data:text/html,<!DOCTYPE html>
|
|
672
|
+
<html lang="en">
|
|
673
|
+
<head><meta charset="UTF-8"><title>Browser Test Page</title></head>
|
|
674
|
+
<body>
|
|
675
|
+
<h1 id="test-title">Browser Test</h1>
|
|
676
|
+
<p id="test-paragraph">Hello, world!</p>
|
|
677
|
+
<button id="test-button" onclick="document.getElementById('test-output').textContent='clicked'">Click Me</button>
|
|
678
|
+
<input id="test-input" type="text" placeholder="Type here">
|
|
679
|
+
<label><input type="checkbox" id="test-checkbox" checked> Checkbox</label>
|
|
680
|
+
<label><input type="radio" name="test-radio" id="radio-1" checked> Radio 1</label>
|
|
681
|
+
<label><input type="radio" name="test-radio" id="radio-2"> Radio 2</label>
|
|
682
|
+
<div id="test-output"></div>
|
|
683
|
+
<script>
|
|
684
|
+
console.log('Browser test page loaded');
|
|
685
|
+
console.error('Test error message');
|
|
686
|
+
</script>
|
|
687
|
+
</body>
|
|
688
|
+
</html>`;
|
|
689
|
+
const baseArgs = { url: dataUrl, sameTab: true, timeout: 30000 };
|
|
690
|
+
async function runDataStep(name, action, extraArgs) {
|
|
691
|
+
if (signal?.aborted) {
|
|
692
|
+
steps.push({ name, status: 'skipped', durationMs: 0, details: 'тест отменён' });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const start = Date.now();
|
|
696
|
+
try {
|
|
697
|
+
const result = await Promise.race([
|
|
698
|
+
executeAction({ ...baseArgs, ...extraArgs, action }),
|
|
699
|
+
new Promise((_resolve, _reject) => setTimeout(() => _reject(new Error('timeout 30s')), 30000)),
|
|
700
|
+
]);
|
|
701
|
+
steps.push({ name, status: result.success ? 'passed' : 'failed', durationMs: Date.now() - start, details: result.success ? (result.output || '').slice(0, 200) : (result.error || 'unknown error') });
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
steps.push({ name, status: 'failed', durationMs: Date.now() - start, details: String(err) });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
await runDataStep('open', 'open', {});
|
|
708
|
+
await runDataStep('html', 'html', {});
|
|
709
|
+
await runDataStep('eval', 'eval', { code: 'document.title' });
|
|
710
|
+
await runDataStep('fill', 'fill', { selector: '#test-input', text: 'test input value' });
|
|
711
|
+
await runDataStep('click (button)', 'click', { selector: '#test-button' });
|
|
712
|
+
await runDataStep('text (after click)', 'text', { selector: '#test-output' });
|
|
713
|
+
await runDataStep('click (checkbox)', 'click', { selector: '#test-checkbox' });
|
|
714
|
+
await runDataStep('click (radio)', 'click', { selector: '#radio-2' });
|
|
715
|
+
await runDataStep('console', 'console', {});
|
|
716
|
+
steps.push({ name: 'storage (local)', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Storage недоступен на data: URL` });
|
|
717
|
+
steps.push({ name: 'storage (session)', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Storage недоступен на data: URL` });
|
|
718
|
+
steps.push({ name: 'cookies', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Cookies недоступны на data: URL` });
|
|
719
|
+
await runDataStep('screenshot', 'shot', {});
|
|
720
|
+
steps.push({ name: 'network', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Network/fetch не проверен` });
|
|
721
|
+
return buildBrowserTestReport(steps, requestedMode, actualHeadless, signal?.aborted ?? false, serverError);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
// Full test with HTTP server
|
|
725
|
+
const baseArgs = { url: testUrl, sameTab: true, timeout: 30000 };
|
|
726
|
+
async function runStep(name, action, extraArgs) {
|
|
727
|
+
if (signal?.aborted) {
|
|
728
|
+
steps.push({ name, status: 'skipped', durationMs: 0, details: 'тест отменён' });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const start = Date.now();
|
|
732
|
+
try {
|
|
733
|
+
const result = await Promise.race([
|
|
734
|
+
executeAction({ ...baseArgs, ...extraArgs, action }),
|
|
735
|
+
new Promise((_resolve, _reject) => setTimeout(() => _reject(new Error('timeout 30s')), 30000)),
|
|
736
|
+
]);
|
|
737
|
+
steps.push({ name, status: result.success ? 'passed' : 'failed', durationMs: Date.now() - start, details: result.success ? (result.output || '').slice(0, 200) : (result.error || 'unknown error') });
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
steps.push({ name, status: 'failed', durationMs: Date.now() - start, details: String(err) });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// 1. open
|
|
744
|
+
await runStep('open', 'open', {});
|
|
745
|
+
// 2. html — read full DOM
|
|
746
|
+
await runStep('html', 'html', {});
|
|
747
|
+
// 3. eval
|
|
748
|
+
await runStep('eval', 'eval', { code: 'document.title' });
|
|
749
|
+
// 4. fill
|
|
750
|
+
await runStep('fill', 'fill', { selector: '#test-input', text: 'test input value' });
|
|
751
|
+
// 5. click button
|
|
752
|
+
await runStep('click (button)', 'click', { selector: '#test-button' });
|
|
753
|
+
// 6. text — read result after click
|
|
754
|
+
await runStep('text (after click)', 'text', { selector: '#test-output' });
|
|
755
|
+
// 7. click checkbox
|
|
756
|
+
await runStep('click (checkbox)', 'click', { selector: '#test-checkbox' });
|
|
757
|
+
// 8. click radio
|
|
758
|
+
await runStep('click (radio)', 'click', { selector: '#radio-2' });
|
|
759
|
+
// 9. console
|
|
760
|
+
await runStep('console', 'console', {});
|
|
761
|
+
// 10. storage (localStorage)
|
|
762
|
+
await runStep('storage (local)', 'storage', { local: true });
|
|
763
|
+
// 11. storage (sessionStorage)
|
|
764
|
+
await runStep('storage (session)', 'storage', { session: true });
|
|
765
|
+
// 12. cookies
|
|
766
|
+
await runStep('cookies', 'cookies', {});
|
|
767
|
+
// 13. screenshot
|
|
768
|
+
await runStep('screenshot', 'shot', {});
|
|
769
|
+
// 14. network
|
|
770
|
+
await runStep('network', 'network', { api: true });
|
|
771
|
+
// Close the HTTP server
|
|
772
|
+
server.close();
|
|
773
|
+
}
|
|
774
|
+
return buildBrowserTestReport(steps, requestedMode, actualHeadless, signal?.aborted ?? false, serverError);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Build the markdown report from collected steps.
|
|
778
|
+
* Extracted so it can be called both from the normal flow and from early-exit on browser init failure.
|
|
779
|
+
*/
|
|
780
|
+
function buildBrowserTestReport(steps, requestedMode, actualHeadless, stoppedEarly, serverError, initFailed) {
|
|
781
|
+
const actualMode = initFailed ? 'не запущен' : (actualHeadless ? 'headless' : 'headed');
|
|
782
|
+
const modeMatch = !initFailed && requestedMode === actualMode;
|
|
783
|
+
const stoppedReason = stoppedEarly ? 'тест отменён пользователем' : undefined;
|
|
784
|
+
// Build summary
|
|
785
|
+
const passed = steps.filter(s => s.status === 'passed').length;
|
|
786
|
+
const failed = steps.filter(s => s.status === 'failed').length;
|
|
787
|
+
const skipped = steps.filter(s => s.status === 'skipped').length;
|
|
788
|
+
// Save structured result
|
|
789
|
+
lastBrowserTestResult = {
|
|
790
|
+
timestamp: new Date().toISOString(),
|
|
791
|
+
mode: initFailed ? 'headless' : actualMode,
|
|
792
|
+
steps,
|
|
793
|
+
summary: { passed, failed, skipped },
|
|
794
|
+
stoppedEarly,
|
|
795
|
+
stoppedReason,
|
|
796
|
+
};
|
|
797
|
+
// Get PID for the report
|
|
798
|
+
const state = chromeManager.getState();
|
|
799
|
+
const pid = initFailed ? undefined : state.managedProcessPid;
|
|
800
|
+
// Build markdown report
|
|
801
|
+
const lines = [
|
|
802
|
+
'## 🧪 Browser Test Report',
|
|
803
|
+
'',
|
|
804
|
+
'> **Verified** — все результаты подтверждены реальными chrome tool calls.',
|
|
805
|
+
`> **Запрошенный режим:** ${requestedMode}`,
|
|
806
|
+
`> **Фактический режим:** ${actualMode}`,
|
|
807
|
+
`> **PID процесса:** ${pid ?? '—'}`,
|
|
808
|
+
'',
|
|
809
|
+
];
|
|
810
|
+
if (initFailed) {
|
|
811
|
+
lines.push('> ❌ **Chrome не удалось запустить.** Browser test не выполнялся.');
|
|
812
|
+
lines.push('');
|
|
813
|
+
}
|
|
814
|
+
else if (!modeMatch) {
|
|
815
|
+
lines.push(`> ❌ **Режимы не совпадают.** Запрошен ${requestedMode}, но Chrome работает в ${actualMode}. Browser test считается failed.`);
|
|
816
|
+
lines.push('');
|
|
817
|
+
}
|
|
818
|
+
if (stoppedEarly) {
|
|
819
|
+
lines.push('> ⚠️ **Тест остановлен досрочно.** Причина: отмена пользователем.');
|
|
820
|
+
lines.push(`> Проверено шагов: ${steps.filter(s => s.status !== 'skipped').length} / ${steps.length}`);
|
|
821
|
+
lines.push('');
|
|
822
|
+
}
|
|
823
|
+
if (serverError) {
|
|
824
|
+
lines.push(`> ⚠️ **HTTP-сервер не поднят:** ${serverError}. Storage/cookies/network пропущены.`);
|
|
825
|
+
lines.push('');
|
|
826
|
+
}
|
|
827
|
+
lines.push('| Шаг | Статус | Длительность | Детали |');
|
|
828
|
+
lines.push('|------|--------|-------------|--------|');
|
|
829
|
+
for (const step of steps) {
|
|
830
|
+
const icon = step.status === 'passed' ? '✅' : step.status === 'failed' ? '❌' : '⏭️';
|
|
831
|
+
const dur = step.durationMs > 0 ? `${step.durationMs}ms` : '—';
|
|
832
|
+
// Escape pipes for markdown table — each step is one row
|
|
833
|
+
let detail = step.details.replace(/\|/g, '\\|');
|
|
834
|
+
if (detail.length > 100) {
|
|
835
|
+
detail = detail.slice(0, 100) + '…';
|
|
836
|
+
}
|
|
837
|
+
lines.push(`| ${icon} ${step.name} | ${step.status} | ${dur} | ${detail} |`);
|
|
838
|
+
}
|
|
839
|
+
lines.push('');
|
|
840
|
+
lines.push(`**Итого:** ${passed} passed, ${failed} failed, ${skipped} skipped`);
|
|
841
|
+
// ✅ What works
|
|
842
|
+
const working = steps.filter(s => s.status === 'passed').map(s => s.name);
|
|
843
|
+
if (working.length > 0) {
|
|
844
|
+
lines.push('');
|
|
845
|
+
lines.push('### ✅ Что работает');
|
|
846
|
+
for (const name of working) {
|
|
847
|
+
lines.push(`- **${name}**`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// ❌ What fails
|
|
851
|
+
const failing = steps.filter(s => s.status === 'failed');
|
|
852
|
+
if (failing.length > 0) {
|
|
853
|
+
lines.push('');
|
|
854
|
+
lines.push('### ❌ Что не работает');
|
|
855
|
+
for (const step of failing) {
|
|
856
|
+
lines.push(`- **${step.name}**: ${step.details}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// 🔧 What to fix
|
|
860
|
+
if (failing.length > 0) {
|
|
861
|
+
lines.push('');
|
|
862
|
+
lines.push('### 🔧 Что доработать');
|
|
863
|
+
for (const step of failing) {
|
|
864
|
+
lines.push(`- Исправить \`${step.name}\`: ${step.details}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// ⏭️ What is skipped
|
|
868
|
+
const skippedSteps = steps.filter(s => s.status === 'skipped');
|
|
869
|
+
if (skippedSteps.length > 0) {
|
|
870
|
+
lines.push('');
|
|
871
|
+
lines.push('### ⏭️ Что пропущено');
|
|
872
|
+
for (const step of skippedSteps) {
|
|
873
|
+
lines.push(`- **${step.name}**: ${step.details}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return lines.join('\n');
|
|
877
|
+
}
|
|
878
|
+
// ─── Real Site Smoke Test ─────────────────────────────────────────────────────
|
|
879
|
+
const DEFAULT_REAL_SITES = ['example.com', 'wikipedia.org', 'github.com'];
|
|
880
|
+
const MAX_REAL_CALLS = 20;
|
|
881
|
+
export async function browserRealTest(options) {
|
|
882
|
+
const desiredHeadless = options?.headless ?? false;
|
|
883
|
+
const signal = options?.signal;
|
|
884
|
+
const rawSites = options?.sites && options.sites.length > 0
|
|
885
|
+
? options.sites
|
|
886
|
+
: DEFAULT_REAL_SITES;
|
|
887
|
+
const sites = rawSites.map(s => {
|
|
888
|
+
const t = s.trim().toLowerCase();
|
|
889
|
+
return t.startsWith('http://') || t.startsWith('https://') ? t : `https://${t}`;
|
|
890
|
+
});
|
|
891
|
+
let chromeCalls = 0;
|
|
892
|
+
const results = [];
|
|
893
|
+
try {
|
|
894
|
+
await chromeManager.ensureMode(desiredHeadless);
|
|
895
|
+
}
|
|
896
|
+
catch (err) {
|
|
897
|
+
return `## /browser-real-test\n\n❌ Chrome не удалось запустить: ${String(err)}`;
|
|
898
|
+
}
|
|
899
|
+
for (const url of sites) {
|
|
900
|
+
if (signal?.aborted) {
|
|
901
|
+
results.push({ site: url, steps: [{ action: 'all', status: 'skipped', detail: 'aborted' }], blocked: false, skipReason: 'aborted' });
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (chromeCalls >= MAX_REAL_CALLS) {
|
|
905
|
+
results.push({ site: url, steps: [{ action: 'all', status: 'skipped', detail: `call limit ${MAX_REAL_CALLS} reached` }], blocked: false, skipReason: 'limit' });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const siteResult = { site: url, steps: [], blocked: false };
|
|
909
|
+
results.push(siteResult);
|
|
910
|
+
async function siteAction(args) {
|
|
911
|
+
chromeCalls++;
|
|
912
|
+
try {
|
|
913
|
+
return await Promise.race([
|
|
914
|
+
executeAction(args),
|
|
915
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error('timeout 15s')), 15000)),
|
|
916
|
+
]);
|
|
917
|
+
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
return { success: false, output: '', error: String(err) };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// 1: open
|
|
923
|
+
if (chromeCalls >= MAX_REAL_CALLS) {
|
|
924
|
+
siteResult.steps.push({ action: 'open', status: 'skipped', detail: 'call limit' });
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const openR = await siteAction({ action: 'open', url, sameTab: true, timeout: 15000 });
|
|
928
|
+
siteResult.steps.push({ action: 'open', status: openR.success ? 'passed' : 'failed', detail: openR.success ? '' : (openR.error ?? 'failed').slice(0, 120) });
|
|
929
|
+
if (!openR.success)
|
|
930
|
+
continue;
|
|
931
|
+
// 2: title via eval (no html action)
|
|
932
|
+
if (chromeCalls >= MAX_REAL_CALLS) {
|
|
933
|
+
siteResult.steps.push({ action: 'title', status: 'skipped', detail: 'call limit' });
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const titleR = await siteAction({ action: 'eval', sameTab: true, code: 'document.title', timeout: 10000 });
|
|
937
|
+
siteResult.steps.push({ action: 'title', status: titleR.success ? 'passed' : 'failed', detail: titleR.success ? (titleR.output || '').slice(0, 80) : (titleR.error ?? '').slice(0, 80) });
|
|
938
|
+
// 3: cookie/captcha/consent wall check
|
|
939
|
+
if (chromeCalls >= MAX_REAL_CALLS) {
|
|
940
|
+
siteResult.steps.push({ action: 'cookie-check', status: 'skipped', detail: 'call limit' });
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const wallCode = '!!document.querySelector(\'[id*="cookie"],[class*="cookie"],[id*="consent"],[class*="consent"],[id*="captcha"],[class*="captcha"],[id*="gdpr"],[class*="gdpr"]\')';
|
|
944
|
+
const wallR = await siteAction({ action: 'eval', sameTab: true, code: wallCode, timeout: 10000 });
|
|
945
|
+
const hasWall = wallR.success && wallR.output.trim() === 'true';
|
|
946
|
+
siteResult.steps.push({ action: 'cookie-check', status: hasWall ? 'blocked' : 'passed', detail: hasWall ? 'cookie/captcha/consent wall detected — skipped' : 'clear' });
|
|
947
|
+
if (hasWall) {
|
|
948
|
+
siteResult.blocked = true;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
// 4: screenshot
|
|
952
|
+
if (chromeCalls >= MAX_REAL_CALLS) {
|
|
953
|
+
siteResult.steps.push({ action: 'screenshot', status: 'skipped', detail: 'call limit' });
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const shotR = await siteAction({ action: 'shot', sameTab: true, timeout: 10000 });
|
|
957
|
+
siteResult.steps.push({ action: 'screenshot', status: shotR.success ? 'passed' : 'failed', detail: shotR.success ? 'ok' : (shotR.error ?? '').slice(0, 80) });
|
|
958
|
+
// 5: console errors
|
|
959
|
+
if (chromeCalls >= MAX_REAL_CALLS) {
|
|
960
|
+
siteResult.steps.push({ action: 'console', status: 'skipped', detail: 'call limit' });
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const consoleR = await siteAction({ action: 'console', sameTab: true, timeout: 10000 });
|
|
964
|
+
siteResult.steps.push({ action: 'console', status: consoleR.success ? 'passed' : 'failed', detail: consoleR.success ? (consoleR.output || 'no errors').slice(0, 120) : (consoleR.error ?? '').slice(0, 80) });
|
|
965
|
+
}
|
|
966
|
+
return buildRealTestReport(results, chromeCalls, sites.length);
|
|
967
|
+
}
|
|
968
|
+
function buildRealTestReport(results, totalCalls, totalSites) {
|
|
969
|
+
const ACTIONS = ['open', 'title', 'cookie-check', 'screenshot', 'console'];
|
|
970
|
+
const allSteps = results.flatMap(r => r.steps);
|
|
971
|
+
const passed = allSteps.filter(s => s.status === 'passed').length;
|
|
972
|
+
const failed = allSteps.filter(s => s.status === 'failed').length;
|
|
973
|
+
const skipped = allSteps.filter(s => s.status === 'skipped').length;
|
|
974
|
+
const blocked = allSteps.filter(s => s.status === 'blocked').length;
|
|
975
|
+
const partial = results.some(r => r.skipReason === 'limit');
|
|
976
|
+
const lines = [
|
|
977
|
+
'## /browser-real-test Results',
|
|
978
|
+
'',
|
|
979
|
+
`**Sites:** ${results.length}/${totalSites} | **Calls:** ${totalCalls}/${MAX_REAL_CALLS} | **Passed:** ${passed} | **Failed:** ${failed} | **Skipped:** ${skipped} | **Blocked:** ${blocked}`,
|
|
980
|
+
'> Token safety: OK — no full HTML reads, call limit enforced',
|
|
981
|
+
'',
|
|
982
|
+
];
|
|
983
|
+
if (partial) {
|
|
984
|
+
lines.push(`> ⚠️ Partial Report: reached limit of ${MAX_REAL_CALLS} calls. Some sites were skipped.`);
|
|
985
|
+
lines.push('');
|
|
986
|
+
}
|
|
987
|
+
lines.push(`| Site | ${ACTIONS.join(' | ')} | Notes |`);
|
|
988
|
+
lines.push(`|------|${ACTIONS.map(() => '------').join('|')}|-------|`);
|
|
989
|
+
for (const r of results) {
|
|
990
|
+
const cells = ACTIONS.map(action => {
|
|
991
|
+
const step = r.steps.find(s => s.action === action);
|
|
992
|
+
if (!step)
|
|
993
|
+
return '⏭️';
|
|
994
|
+
if (step.status === 'passed')
|
|
995
|
+
return '✅';
|
|
996
|
+
if (step.status === 'failed')
|
|
997
|
+
return '❌';
|
|
998
|
+
if (step.status === 'blocked')
|
|
999
|
+
return '🚫';
|
|
1000
|
+
return '⏭️';
|
|
1001
|
+
});
|
|
1002
|
+
const domain = r.site.replace(/^https?:\/\//, '');
|
|
1003
|
+
const notes = r.blocked
|
|
1004
|
+
? 'cookie/consent wall'
|
|
1005
|
+
: r.skipReason === 'limit'
|
|
1006
|
+
? 'call limit'
|
|
1007
|
+
: r.skipReason === 'aborted'
|
|
1008
|
+
? 'aborted'
|
|
1009
|
+
: '—';
|
|
1010
|
+
lines.push(`| ${domain} | ${cells.join(' | ')} | ${notes} |`);
|
|
1011
|
+
}
|
|
1012
|
+
const failedSteps = results.flatMap(r => r.steps.filter(s => s.status === 'failed').map(s => ({ site: r.site, ...s })));
|
|
1013
|
+
if (failedSteps.length > 0) {
|
|
1014
|
+
lines.push('');
|
|
1015
|
+
lines.push('### Failed Actions');
|
|
1016
|
+
lines.push('| Site | Action | Reason | Blocker |');
|
|
1017
|
+
lines.push('|------|--------|--------|---------|');
|
|
1018
|
+
for (const s of failedSteps) {
|
|
1019
|
+
const domain = s.site.replace(/^https?:\/\//, '');
|
|
1020
|
+
lines.push(`| ${domain} | ${s.action} | ${s.detail.replace(/\|/g, '\\|')} | ${s.action === 'open' ? 'yes' : 'no'} |`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
lines.push('');
|
|
1024
|
+
lines.push(`**Что доработать:** ${failedSteps.length === 0 ? 'нет проблем' : failedSteps.map(s => s.action).join(', ')}`);
|
|
1025
|
+
return lines.join('\n');
|
|
1026
|
+
}
|
|
1027
|
+
export const chromeTool = {
|
|
1028
|
+
name: 'chrome',
|
|
1029
|
+
description: `Control a real browser through a native Chrome runtime.
|
|
1030
|
+
Use it for UI validation, rendered DOM inspection, console and network debugging,
|
|
1031
|
+
screenshots, and multi-step browser workflows when terminal tools are not enough.
|
|
1032
|
+
If the task mentions localhost pages, forms, browser bugs, screenshots, rendered state,
|
|
1033
|
+
console output, or network requests, this is the primary tool.
|
|
1034
|
+
|
|
1035
|
+
Examples:
|
|
1036
|
+
- { "action": "open", "url": "https://example.com" }
|
|
1037
|
+
- { "action": "text", "url": "https://example.com", "selector": "h1" }
|
|
1038
|
+
- { "action": "click", "url": "https://example.com", "selector": ".submit-btn" }
|
|
1039
|
+
- { "action": "fill", "url": "https://example.com/login", "selector": "#email", "text": "user@example.com" }
|
|
1040
|
+
- { "action": "eval", "url": "https://example.com", "code": "document.title" }
|
|
1041
|
+
- { "action": "network", "url": "https://example.com", "api": true }
|
|
1042
|
+
- { "action": "state" }
|
|
1043
|
+
- { "action": "shot", "url": "https://example.com", "output": "screenshot.png", "full": true }
|
|
1044
|
+
- Use "sameTab": true for multi-step flows in one tab.
|
|
1045
|
+
- Use "headless": true for automation or CI-safe browser execution.`,
|
|
1046
|
+
parameters: chromeParameters,
|
|
1047
|
+
execute: async (args) => {
|
|
1048
|
+
const typedArgs = args;
|
|
1049
|
+
if (!typedArgs.action) {
|
|
1050
|
+
return { success: false, output: '', error: 'Action is required' };
|
|
1051
|
+
}
|
|
1052
|
+
if (typedArgs.port) {
|
|
1053
|
+
chromeManager.setDebugPort(typedArgs.port);
|
|
1054
|
+
}
|
|
1055
|
+
return executeAction(typedArgs);
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
//# sourceMappingURL=chrome.js.map
|