testronaut 1.0.25 → 1.0.27
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/core/turnLoop.js +27 -0
- package/package.json +1 -1
- package/tools/chromeBrowser.js +252 -21
- package/tools/toolSchema.js +108 -49
package/core/turnLoop.js
CHANGED
|
@@ -21,6 +21,14 @@ let turnTimestamps = [];
|
|
|
21
21
|
let shouldBackoff;
|
|
22
22
|
let steps = [];
|
|
23
23
|
|
|
24
|
+
function formatBytes(n) {
|
|
25
|
+
if (!Number.isFinite(n)) return `${n}`;
|
|
26
|
+
const u = ['B','KB','MB','GB','TB'];
|
|
27
|
+
let i = 0, v = n;
|
|
28
|
+
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
|
29
|
+
return `${v.toFixed(v >= 10 || i === 0 ? 0 : 1)} ${u[i]}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool } = {}) => {
|
|
25
33
|
if (skipIfLastTool && skipIfLastTool.includes(messages.at(-1)?.name)) {
|
|
26
34
|
console.log(`[skip] Skipping DOM push after redundant tool: ${messages.at(-1)?.name}`);
|
|
@@ -187,6 +195,25 @@ export const turnLoop = async (
|
|
|
187
195
|
result = errorMessage;
|
|
188
196
|
}
|
|
189
197
|
|
|
198
|
+
// ------ Capture file upload/download events for mission report ------
|
|
199
|
+
try {
|
|
200
|
+
const maybeJson = JSON.parse(result);
|
|
201
|
+
if (maybeJson && maybeJson._testronaut_file_event) {
|
|
202
|
+
currentStep.files = currentStep.files || [];
|
|
203
|
+
currentStep.files.push(maybeJson);
|
|
204
|
+
|
|
205
|
+
if (maybeJson._testronaut_file_event === 'upload') {
|
|
206
|
+
const msgLine = `📤 Uploaded "${maybeJson.fileName}" (${formatBytes(maybeJson.bytes)}) via ${maybeJson.method}`;
|
|
207
|
+
currentStep.events.push(msgLine);
|
|
208
|
+
console.log(msgLine);
|
|
209
|
+
} else if (maybeJson._testronaut_file_event === 'download') {
|
|
210
|
+
const msgLine = `📥 Downloaded "${maybeJson.fileName}" (${formatBytes(maybeJson.bytes)}) via ${maybeJson.mode}`;
|
|
211
|
+
currentStep.events.push(msgLine);
|
|
212
|
+
console.log(msgLine);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch { /* not JSON / ignore */ }
|
|
216
|
+
|
|
190
217
|
console.log(`[tool ] ← ${fnName} result:`, errorMessage ? '❌ Failed' : '✅ Success');
|
|
191
218
|
currentStep.events.push(`[tool ] ← ${fnName} result: ${errorMessage ? '❌ Failed' : '✅ Success'}`)
|
|
192
219
|
const truncated = result.length > 100 ? result.slice(0, 100) + '…' : result;
|
package/package.json
CHANGED
package/tools/chromeBrowser.js
CHANGED
|
@@ -19,8 +19,26 @@ import {
|
|
|
19
19
|
removeObfuscatedClassNames,
|
|
20
20
|
} from './domControl.js';
|
|
21
21
|
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
22
23
|
import { ensureBrowsers } from '../tools/playwrightSetup.js';
|
|
23
24
|
|
|
25
|
+
const FILES_DIR = path.join('missions', 'files');
|
|
26
|
+
const REPORTS_DIR = path.join('missions', 'mission_reports');
|
|
27
|
+
const FILES_LOG = path.join(REPORTS_DIR, 'files.jsonl');
|
|
28
|
+
|
|
29
|
+
function ensureDir(p) {
|
|
30
|
+
try { fs.mkdirSync(p, { recursive: true }); } catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendFileEvent(eventObj = {}) {
|
|
34
|
+
try {
|
|
35
|
+
ensureDir(REPORTS_DIR);
|
|
36
|
+
fs.appendFileSync(FILES_LOG, JSON.stringify(eventObj) + '\n', 'utf8');
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.warn('⚠️ Could not append file event:', e?.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
24
42
|
export class ChromeBrowser {
|
|
25
43
|
constructor() {
|
|
26
44
|
this.browser = null;
|
|
@@ -504,11 +522,24 @@ export class ChromeBrowser {
|
|
|
504
522
|
throw new Error('Could not input text with any strategy');
|
|
505
523
|
}
|
|
506
524
|
|
|
525
|
+
_chromiumLaunchArgsForCI() {
|
|
526
|
+
const inCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.TESTRONAUT_CI_CHROMIUM);
|
|
527
|
+
return inCI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [];
|
|
528
|
+
}
|
|
529
|
+
|
|
507
530
|
|
|
508
531
|
async start() {
|
|
509
532
|
try {
|
|
510
533
|
const pw = await loadPlaywright();
|
|
511
|
-
|
|
534
|
+
const launchArgs = [
|
|
535
|
+
'--disable-dev-shm-usage',
|
|
536
|
+
'--disable-gpu',
|
|
537
|
+
...this._chromiumLaunchArgsForCI(),
|
|
538
|
+
];
|
|
539
|
+
this.browser = await pw.chromium.launch({
|
|
540
|
+
headless: true,
|
|
541
|
+
args: launchArgs,
|
|
542
|
+
});
|
|
512
543
|
} catch (err) {
|
|
513
544
|
const msg = String(err?.message || err);
|
|
514
545
|
const needsInstall =
|
|
@@ -525,9 +556,24 @@ export class ChromeBrowser {
|
|
|
525
556
|
|
|
526
557
|
// Retry
|
|
527
558
|
const pw = await loadPlaywright();
|
|
528
|
-
|
|
559
|
+
const launchArgs = [
|
|
560
|
+
'--disable-dev-shm-usage',
|
|
561
|
+
'--disable-gpu',
|
|
562
|
+
...this._chromiumLaunchArgsForCI(),
|
|
563
|
+
];
|
|
564
|
+
this.browser = await pw.chromium.launch({
|
|
565
|
+
headless: true,
|
|
566
|
+
args: launchArgs,
|
|
567
|
+
});
|
|
568
|
+
// return await pw.chromium.launch({ headless: true });
|
|
529
569
|
}
|
|
530
|
-
|
|
570
|
+
|
|
571
|
+
ensureDir(FILES_DIR); // ⬅️ make sure missions/files exists
|
|
572
|
+
|
|
573
|
+
this.context = await this.browser.newContext({
|
|
574
|
+
acceptDownloads: true // ⬅️ important for Playwright download handling
|
|
575
|
+
});
|
|
576
|
+
// this.context = await this.browser.newContext();
|
|
531
577
|
const first = await this.context.newPage();
|
|
532
578
|
const id = this._addAndFocus(first);
|
|
533
579
|
|
|
@@ -740,34 +786,97 @@ export class ChromeBrowser {
|
|
|
740
786
|
|
|
741
787
|
|
|
742
788
|
|
|
743
|
-
async get_dom(
|
|
744
|
-
|
|
789
|
+
async get_dom(
|
|
790
|
+
{ limit = 100000, exclude = true, focus = [], loadState = 'networkidle', timeout = 10000 } = {},
|
|
791
|
+
agentMemory = {}
|
|
792
|
+
) {
|
|
793
|
+
// --- 1) Ensure we have a live page ---
|
|
794
|
+
const hasContext = !!this.context;
|
|
795
|
+
|
|
796
|
+
// Recover a page if missing/closed
|
|
797
|
+
const recoverPage = async () => {
|
|
798
|
+
if (hasContext) {
|
|
799
|
+
const pages = (this.context.pages?.() || []).filter(p => !p.isClosed?.());
|
|
800
|
+
if (pages.length) {
|
|
801
|
+
// Prefer the last page (often the active one / popup)
|
|
802
|
+
this.page = pages[pages.length - 1];
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// No pages at all in a live context -> make one
|
|
806
|
+
this.page = await this.context.newPage();
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// No context yet but a browser exists -> create context + page
|
|
810
|
+
if (this.browser?.newContext) {
|
|
811
|
+
this.context = await this.browser.newContext();
|
|
812
|
+
this.page = await this.context.newPage();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
throw new Error('No browser context/page available in get_dom().');
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
if (!this.page || this.page.isClosed?.()) {
|
|
819
|
+
await recoverPage();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// --- 2) Try to reach a usable load state (best-effort) ---
|
|
745
823
|
try {
|
|
746
|
-
|
|
747
|
-
} catch
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
824
|
+
await this.page.waitForLoadState(loadState, { timeout });
|
|
825
|
+
} catch {
|
|
826
|
+
// Non-fatal: some apps never reach 'load', we’ll still try to read the DOM.
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// If navigation changed active page (e.g., popup), reselect a live page
|
|
830
|
+
try {
|
|
831
|
+
const pages = this.context?.pages?.() || [];
|
|
832
|
+
if (pages.length && (this.page.isClosed?.() || !pages.includes(this.page))) {
|
|
833
|
+
this.page = pages[pages.length - 1];
|
|
834
|
+
}
|
|
835
|
+
} catch { /* ignore */ }
|
|
836
|
+
|
|
837
|
+
// --- 3) Get HTML content with a small retry on failure ---
|
|
838
|
+
let html = '';
|
|
839
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
840
|
+
try {
|
|
841
|
+
html = await this.page.content();
|
|
842
|
+
if (html && html.trim()) break;
|
|
843
|
+
} catch (err) {
|
|
844
|
+
// First failure: try waiting for network to settle, then retry
|
|
845
|
+
if (attempt === 0) {
|
|
846
|
+
try { await this.page.waitForLoadState('networkidle', { timeout: 5000 }); } catch {}
|
|
847
|
+
// Also re-check active page (popups can swap)
|
|
848
|
+
try {
|
|
849
|
+
const pages = this.context?.pages?.() || [];
|
|
850
|
+
if (pages.length) this.page = pages[pages.length - 1];
|
|
851
|
+
} catch {}
|
|
852
|
+
} else {
|
|
853
|
+
// Second failure: bubble up a clearer message
|
|
854
|
+
throw new Error(`get_dom() failed to read page content: ${err?.message || err}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
752
857
|
}
|
|
858
|
+
|
|
859
|
+
// Fallback to empty string if still nothing (keeps caller logic simple)
|
|
860
|
+
if (!html) html = '';
|
|
861
|
+
|
|
862
|
+
// --- 4) Optional DOM reduction/cleanup (your existing steps) ---
|
|
753
863
|
let $ = cheerio.load(html);
|
|
754
|
-
|
|
755
|
-
// Remove non-content tags
|
|
864
|
+
|
|
756
865
|
if (exclude) {
|
|
757
866
|
$ = await removeNonContentTags($);
|
|
758
|
-
// console.log('🔧 Non-content tags removed from DOM.');
|
|
759
867
|
$ = await reduceRepeatedDivs($);
|
|
760
|
-
//
|
|
868
|
+
// If you later re-enable:
|
|
761
869
|
// $ = await removeObfuscatedClassNames($);
|
|
762
|
-
// console.log('🔧 Obfuscated class names removed from DOM.')
|
|
763
870
|
// $ = await removeAutoGeneratedClasses($);
|
|
764
|
-
// console.log('🔧 Auto-generated classes removed from DOM.');
|
|
765
871
|
}
|
|
766
|
-
|
|
872
|
+
|
|
873
|
+
// (Optional: If you'd like to use `focus`, you could filter here by selectors/keywords.)
|
|
874
|
+
|
|
875
|
+
// --- 5) Return trimmed HTML ---
|
|
767
876
|
const finalHtml = $.html();
|
|
768
|
-
|
|
769
|
-
return trimmed;
|
|
877
|
+
return finalHtml.slice(0, limit);
|
|
770
878
|
}
|
|
879
|
+
|
|
771
880
|
|
|
772
881
|
async check_text({ text }) {
|
|
773
882
|
const html = await this.page.content();
|
|
@@ -906,7 +1015,127 @@ export class ChromeBrowser {
|
|
|
906
1015
|
return `Screenshot saved at: ./screenshots/${name}_${timestamp}.png`;
|
|
907
1016
|
}
|
|
908
1017
|
|
|
909
|
-
|
|
1018
|
+
async upload_file({ selector, fileName, useChooser = false, timeoutMs = 15000 }) {
|
|
1019
|
+
if (!selector) throw new Error('upload_file: selector is required');
|
|
1020
|
+
if (!fileName) throw new Error('upload_file: fileName is required');
|
|
1021
|
+
|
|
1022
|
+
ensureDir(FILES_DIR);
|
|
1023
|
+
const filePath = path.resolve(FILES_DIR, fileName);
|
|
1024
|
+
if (!fs.existsSync(filePath)) {
|
|
1025
|
+
throw new Error(`upload_file: file not found at ${filePath}`);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
await this.page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1029
|
+
|
|
1030
|
+
let method = 'input';
|
|
1031
|
+
if (useChooser) {
|
|
1032
|
+
method = 'chooser';
|
|
1033
|
+
const [fileChooser] = await Promise.all([
|
|
1034
|
+
this.page.waitForEvent('filechooser', { timeout: timeoutMs }),
|
|
1035
|
+
this.page.click(selector, { timeout: timeoutMs })
|
|
1036
|
+
]);
|
|
1037
|
+
await fileChooser.setFiles(filePath);
|
|
1038
|
+
} else {
|
|
1039
|
+
const input = this.page.locator(selector).first();
|
|
1040
|
+
await input.waitFor({ state: 'visible', timeout: timeoutMs }).catch(() => {});
|
|
1041
|
+
await input.setInputFiles(filePath, { timeout: timeoutMs });
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const stats = fs.statSync(filePath);
|
|
1045
|
+
const payload = {
|
|
1046
|
+
_testronaut_file_event: 'upload',
|
|
1047
|
+
method, // 'input' | 'chooser'
|
|
1048
|
+
fileName,
|
|
1049
|
+
sourcePath: filePath,
|
|
1050
|
+
bytes: stats.size,
|
|
1051
|
+
selector,
|
|
1052
|
+
ts: new Date().toISOString()
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
appendFileEvent(payload);
|
|
1056
|
+
return JSON.stringify(payload);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
async download_file({ selector, url, fileName, timeoutMs = 30000 }) {
|
|
1061
|
+
ensureDir(FILES_DIR);
|
|
1062
|
+
|
|
1063
|
+
if (!selector && !url) {
|
|
1064
|
+
throw new Error('download_file: provide either selector or url');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const sanitize = (name) => name.replace(/[\\/:*?"<>|]+/g, '_').trim();
|
|
1068
|
+
|
|
1069
|
+
// Mode 1: page click triggers a browser download
|
|
1070
|
+
if (selector) {
|
|
1071
|
+
await this.page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1072
|
+
|
|
1073
|
+
const [download] = await Promise.all([
|
|
1074
|
+
this.page.waitForEvent('download', { timeout: timeoutMs }),
|
|
1075
|
+
this.page.click(selector)
|
|
1076
|
+
]);
|
|
1077
|
+
|
|
1078
|
+
const suggested = sanitize(download.suggestedFilename?.() || 'download.bin');
|
|
1079
|
+
const targetPath = path.join(FILES_DIR, sanitize(fileName || suggested));
|
|
1080
|
+
await download.saveAs(targetPath);
|
|
1081
|
+
|
|
1082
|
+
const stats = fs.statSync(targetPath);
|
|
1083
|
+
const payload = {
|
|
1084
|
+
_testronaut_file_event: 'download',
|
|
1085
|
+
mode: 'click',
|
|
1086
|
+
fileName: path.basename(targetPath),
|
|
1087
|
+
destPath: targetPath,
|
|
1088
|
+
bytes: stats.size,
|
|
1089
|
+
selector,
|
|
1090
|
+
url: null,
|
|
1091
|
+
ts: new Date().toISOString()
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
appendFileEvent(payload);
|
|
1095
|
+
return JSON.stringify(payload);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Mode 2: direct URL fetch (no page interaction)
|
|
1099
|
+
const res = await fetch(url);
|
|
1100
|
+
if (!res.ok) {
|
|
1101
|
+
throw new Error(`download_file: GET ${url} failed with ${res.status}`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Try to infer a filename if not provided
|
|
1105
|
+
const cd = res.headers.get('content-disposition') || '';
|
|
1106
|
+
let inferred = 'download.bin';
|
|
1107
|
+
const match = /filename\*=UTF-8''([^;]+)|filename="([^"]+)"/i.exec(cd);
|
|
1108
|
+
if (match) {
|
|
1109
|
+
inferred = decodeURIComponent(match[1] || match[2]);
|
|
1110
|
+
} else {
|
|
1111
|
+
try {
|
|
1112
|
+
const u = new URL(url);
|
|
1113
|
+
const tail = u.pathname.split('/').filter(Boolean).pop();
|
|
1114
|
+
if (tail) inferred = tail;
|
|
1115
|
+
} catch {}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const targetPath = path.join(FILES_DIR, sanitize(fileName || inferred));
|
|
1119
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1120
|
+
fs.writeFileSync(targetPath, buf);
|
|
1121
|
+
|
|
1122
|
+
const stats = fs.statSync(targetPath);
|
|
1123
|
+
const payload = {
|
|
1124
|
+
_testronaut_file_event: 'download',
|
|
1125
|
+
mode: 'url',
|
|
1126
|
+
fileName: path.basename(targetPath),
|
|
1127
|
+
destPath: targetPath,
|
|
1128
|
+
bytes: stats.size,
|
|
1129
|
+
selector: null,
|
|
1130
|
+
url,
|
|
1131
|
+
ts: new Date().toISOString()
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
appendFileEvent(payload);
|
|
1135
|
+
return JSON.stringify(payload);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
|
|
910
1139
|
async close() {
|
|
911
1140
|
await this.browser?.close();
|
|
912
1141
|
}
|
|
@@ -931,4 +1160,6 @@ export const CHROME_TOOL_MAP = {
|
|
|
931
1160
|
click_and_follow_popup: (b, args) => b.click_and_follow_popup(args),
|
|
932
1161
|
switch_to_page: (b, args) => b.switch_to_page(args),
|
|
933
1162
|
close_current_page: (b, args) => b.close_current_page(args),
|
|
1163
|
+
upload_file: (b, args) => b.upload_file(args),
|
|
1164
|
+
download_file: (b, args) => b.download_file(args),
|
|
934
1165
|
};
|
package/tools/toolSchema.js
CHANGED
|
@@ -174,59 +174,118 @@ const toolsSchema = [
|
|
|
174
174
|
},
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
177
|
+
type: 'function',
|
|
178
|
+
function: {
|
|
179
|
+
name: 'click_and_follow_popup',
|
|
180
|
+
description:
|
|
181
|
+
'Click a selector that opens a new tab/window (e.g., OAuth) and switch control to it. Falls back to same-tab navigation if no popup appears.',
|
|
182
|
+
parameters: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
selector: { type: 'string', description: 'CSS or text selector to click' },
|
|
186
|
+
expectedUrlHost: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: "Optional hostname hint like 'accounts.google.com'"
|
|
189
|
+
},
|
|
190
|
+
timeoutMs: {
|
|
191
|
+
type: 'number',
|
|
192
|
+
default: 20000,
|
|
193
|
+
description: 'How long to wait for the popup or navigation'
|
|
194
|
+
}
|
|
189
195
|
},
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
default: 20000,
|
|
193
|
-
description: 'How long to wait for the popup or navigation'
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
required: ['selector']
|
|
196
|
+
required: ['selector']
|
|
197
|
+
}
|
|
197
198
|
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: 'function',
|
|
202
|
+
function: {
|
|
203
|
+
name: 'switch_to_page',
|
|
204
|
+
description:
|
|
205
|
+
"Switch agent focus between known tabs/pages (e.g., back to 'main' or to the latest popup). Use a pageId previously returned by click_and_follow_popup to target a specific tab.",
|
|
206
|
+
parameters: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
target: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: "One of: 'main', 'latest', or a pageId returned by click_and_follow_popup"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
required: ['target']
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'function',
|
|
220
|
+
function: {
|
|
221
|
+
name: 'close_current_page',
|
|
222
|
+
description:
|
|
223
|
+
'Close the current tab/page and automatically switch back to another open page if available.',
|
|
224
|
+
parameters: { type: 'object', properties: {} }
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'function',
|
|
229
|
+
function: {
|
|
230
|
+
name: 'upload_file',
|
|
231
|
+
description:
|
|
232
|
+
'Upload a file into an <input type="file"> or via a file chooser. Defaults to missions/files as the file source.',
|
|
233
|
+
parameters: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
selector: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'CSS selector for the file input or a button that opens a file chooser'
|
|
239
|
+
},
|
|
240
|
+
fileName: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'File name located in missions/files (e.g., "sample.pdf")'
|
|
243
|
+
},
|
|
244
|
+
useChooser: {
|
|
245
|
+
type: 'boolean',
|
|
246
|
+
default: false,
|
|
247
|
+
description: 'If true, click the selector and fulfill the file chooser instead of setInputFiles'
|
|
248
|
+
},
|
|
249
|
+
timeoutMs: {
|
|
250
|
+
type: 'number',
|
|
251
|
+
default: 15000
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
required: ['selector', 'fileName']
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: 'function',
|
|
260
|
+
function: {
|
|
261
|
+
name: 'download_file',
|
|
262
|
+
description:
|
|
263
|
+
'Download a file to missions/files. Either click a selector that triggers a download or fetch a direct URL.',
|
|
264
|
+
parameters: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
selector: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'An element to click that triggers a download'
|
|
270
|
+
},
|
|
271
|
+
url: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: 'Direct URL to download without clicking the page'
|
|
274
|
+
},
|
|
275
|
+
fileName: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
description: 'Optional override for the saved filename'
|
|
278
|
+
},
|
|
279
|
+
timeoutMs: {
|
|
280
|
+
type: 'number',
|
|
281
|
+
default: 30000
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
// At least one of selector or url should be provided (enforced in code)
|
|
285
|
+
required: []
|
|
286
|
+
}
|
|
215
287
|
}
|
|
216
288
|
}
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
type: 'function',
|
|
220
|
-
function: {
|
|
221
|
-
name: 'close_current_page',
|
|
222
|
-
description:
|
|
223
|
-
'Close the current tab/page and automatically switch back to another open page if available.',
|
|
224
|
-
parameters: { type: 'object', properties: {} }
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
289
|
];
|
|
231
290
|
|
|
232
291
|
export default toolsSchema;
|