ft-scout 9.0.4 → 9.0.5
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/.firebase/hosting.d2Vi.cache +7 -7
- package/README.md +2 -2
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +21 -8
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +253 -13
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/computer/computerController.d.ts.map +1 -1
- package/bin/src/engine/computer/computerController.js +532 -296
- package/bin/src/engine/computer/computerController.js.map +1 -1
- package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
- package/bin/src/engine/computer/computerDiagnostic.js +71 -0
- package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
- package/bin/src/engine/computer/index.d.ts.map +1 -1
- package/bin/src/engine/computer/index.js +1 -0
- package/bin/src/engine/computer/index.js.map +1 -1
- package/bin/src/engine/computer/mockComputer.d.ts.map +1 -1
- package/bin/src/engine/computer/mockComputer.js +104 -8
- package/bin/src/engine/computer/mockComputer.js.map +1 -1
- package/bin/src/engine/computer/scoutDriver.cs +873 -0
- package/bin/src/engine/computer/scout_driver.exe +0 -0
- package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.js +351 -20
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
- package/bin/src/engine/safety.d.ts.map +1 -1
- package/bin/src/engine/safety.js +13 -3
- package/bin/src/engine/safety.js.map +1 -1
- package/bin/src/engine/toolRegistry.d.ts.map +1 -1
- package/bin/src/engine/toolRegistry.js +144 -0
- package/bin/src/engine/toolRegistry.js.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.js +21 -0
- package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js +14 -1
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/package.json +1 -1
- package/web/app.js +2 -2
- package/web/styles.css +2 -2
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { exec } from 'child_process';
|
|
3
|
+
import { exec, execFile, spawn } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
5
8
|
const execAsync = promisify(exec);
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
6
10
|
export class SystemComputerController {
|
|
7
11
|
workspaceDir;
|
|
8
12
|
screenshotsDir;
|
|
9
13
|
expectedApplication = null;
|
|
10
14
|
activeWindow = null;
|
|
15
|
+
driverExePath = null;
|
|
16
|
+
driverInitAttempted = false;
|
|
11
17
|
constructor(workspaceDir = process.cwd()) {
|
|
12
18
|
this.workspaceDir = workspaceDir;
|
|
13
19
|
this.screenshotsDir = path.join(this.workspaceDir, '.ft', 'screenshots');
|
|
@@ -24,47 +30,224 @@ export class SystemComputerController {
|
|
|
24
30
|
getExpectedApplication() {
|
|
25
31
|
return this.expectedApplication;
|
|
26
32
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Resolves or builds the native scout_driver.exe for Windows.
|
|
35
|
+
*/
|
|
36
|
+
async ensureDriver() {
|
|
37
|
+
if (this.driverExePath && fs.existsSync(this.driverExePath)) {
|
|
38
|
+
return this.driverExePath;
|
|
39
|
+
}
|
|
40
|
+
if (process.platform !== 'win32') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Candidate locations
|
|
44
|
+
const candidates = [
|
|
45
|
+
path.join(__dirname, 'scout_driver.exe'),
|
|
46
|
+
path.join(this.workspaceDir, 'bin', 'src', 'engine', 'computer', 'scout_driver.exe'),
|
|
47
|
+
path.join(this.workspaceDir, '.ft', 'bin', 'scout_driver.exe'),
|
|
48
|
+
];
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
if (fs.existsSync(c)) {
|
|
51
|
+
this.driverExePath = c;
|
|
52
|
+
return c;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (this.driverInitAttempted) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
this.driverInitAttempted = true;
|
|
59
|
+
// Try compiling using csc.exe
|
|
60
|
+
const csSourceCandidates = [
|
|
61
|
+
path.join(__dirname, 'scoutDriver.cs'),
|
|
62
|
+
path.join(this.workspaceDir, 'bin', 'src', 'engine', 'computer', 'scoutDriver.cs'),
|
|
63
|
+
];
|
|
64
|
+
const csSource = csSourceCandidates.find((p) => fs.existsSync(p));
|
|
65
|
+
const cscExe = 'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe';
|
|
66
|
+
if (csSource && fs.existsSync(cscExe)) {
|
|
67
|
+
const outDir = path.join(this.workspaceDir, '.ft', 'bin');
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(outDir))
|
|
70
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
71
|
+
const targetExe = path.join(outDir, 'scout_driver.exe');
|
|
72
|
+
await execAsync(`"${cscExe}" /nologo /optimize /t:exe /out:"${targetExe}" /r:System.Windows.Forms.dll,System.Drawing.dll "${csSource}"`);
|
|
73
|
+
if (fs.existsSync(targetExe)) {
|
|
74
|
+
this.driverExePath = targetExe;
|
|
75
|
+
return targetExe;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Runs an action using native driver or fallback.
|
|
84
|
+
*/
|
|
85
|
+
async runDriverAction(action, args = []) {
|
|
86
|
+
const driver = await this.ensureDriver();
|
|
87
|
+
const strArgs = args.map((a) => String(a));
|
|
88
|
+
if (driver) {
|
|
89
|
+
try {
|
|
90
|
+
const { stdout } = await execFileAsync(driver, [action, ...strArgs], {
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
timeout: 10000,
|
|
93
|
+
});
|
|
94
|
+
const trimmed = (stdout || '').trim();
|
|
95
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(trimmed);
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
}
|
|
101
|
+
return { success: true, output: trimmed };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
output: `Driver execution error: ${err?.message || String(err)}`,
|
|
107
|
+
error: err?.message || String(err),
|
|
108
|
+
recoverable: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// PowerShell fallback with input desktop attachment
|
|
113
|
+
return await this.runPowerShellFallback(action, strArgs);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Reliable PowerShell fallback with thread input desktop attachment
|
|
117
|
+
*/
|
|
118
|
+
async runPowerShellFallback(action, args) {
|
|
119
|
+
const script = `
|
|
120
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
121
|
+
Add-Type -AssemblyName System.Drawing
|
|
122
|
+
|
|
123
|
+
$src = @"
|
|
124
|
+
using System;
|
|
125
|
+
using System.Drawing;
|
|
126
|
+
using System.Drawing.Imaging;
|
|
127
|
+
using System.Threading;
|
|
128
|
+
using System.Runtime.InteropServices;
|
|
129
|
+
public class WinDriverFallback {
|
|
130
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
131
|
+
public static extern IntPtr OpenWindowStation(string lpszWinSta, bool fInherit, uint dwDesiredAccess);
|
|
132
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
133
|
+
public static extern bool SetProcessWindowStation(IntPtr hWinSta);
|
|
134
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
135
|
+
public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
136
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
137
|
+
public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
138
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
139
|
+
public static extern bool SetThreadDesktop(IntPtr hDesktop);
|
|
140
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
141
|
+
public static extern bool GetCursorPos(out POINT lpPoint);
|
|
142
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
143
|
+
public static extern bool SetCursorPos(int X, int Y);
|
|
144
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
145
|
+
public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
|
|
146
|
+
[DllImport("user32.dll")]
|
|
147
|
+
public static extern int GetSystemMetrics(int nIndex);
|
|
148
|
+
[DllImport("user32.dll")]
|
|
149
|
+
public static extern bool SetProcessDPIAware();
|
|
150
|
+
|
|
151
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
152
|
+
public struct POINT { public int X; public int Y; }
|
|
153
|
+
|
|
154
|
+
public static void Attach() {
|
|
155
|
+
try {
|
|
156
|
+
SetProcessDPIAware();
|
|
157
|
+
IntPtr hw = OpenWindowStation("WinSta0", false, 0x10000000);
|
|
158
|
+
if (hw != IntPtr.Zero) SetProcessWindowStation(hw);
|
|
159
|
+
IntPtr hd = OpenInputDesktop(0, false, 0x10000000);
|
|
160
|
+
if (hd == IntPtr.Zero) hd = OpenDesktop("Default", 0, false, 0x10000000);
|
|
161
|
+
if (hd != IntPtr.Zero) SetThreadDesktop(hd);
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
"@
|
|
166
|
+
Add-Type -TypeDefinition $src -ReferencedAssemblies System.Drawing
|
|
167
|
+
|
|
168
|
+
[WinDriverFallback]::Attach()
|
|
169
|
+
$act = "${action.replace(/"/g, '`"')}".ToLower()
|
|
170
|
+
|
|
171
|
+
if ($act -eq "move") {
|
|
172
|
+
$x = [int]${Number(args[0]) || 0}
|
|
173
|
+
$y = [int]${Number(args[1]) || 0}
|
|
174
|
+
[WinDriverFallback]::SetCursorPos($x, $y)
|
|
175
|
+
Write-Output "{\`"success\`":true,\`"action\`":\`"move\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
|
|
176
|
+
} elseif ($act -eq "click" -or $act -eq "left_click") {
|
|
177
|
+
$x = [int]${Number(args[0]) || 0}
|
|
178
|
+
$y = [int]${Number(args[1]) || 0}
|
|
179
|
+
[WinDriverFallback]::SetCursorPos($x, $y)
|
|
180
|
+
Start-Sleep -Milliseconds 30
|
|
181
|
+
[WinDriverFallback]::mouse_event(0x0002, 0, 0, 0, 0)
|
|
182
|
+
Start-Sleep -Milliseconds 40
|
|
183
|
+
[WinDriverFallback]::mouse_event(0x0004, 0, 0, 0, 0)
|
|
184
|
+
Write-Output "{\`"success\`":true,\`"action\`":\`"click\`",\`"button\`":\`"left\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
|
|
185
|
+
} elseif ($act -eq "right_click") {
|
|
186
|
+
$x = [int]${Number(args[0]) || 0}
|
|
187
|
+
$y = [int]${Number(args[1]) || 0}
|
|
188
|
+
[WinDriverFallback]::SetCursorPos($x, $y)
|
|
189
|
+
Start-Sleep -Milliseconds 30
|
|
190
|
+
[WinDriverFallback]::mouse_event(0x0008, 0, 0, 0, 0)
|
|
191
|
+
Start-Sleep -Milliseconds 40
|
|
192
|
+
[WinDriverFallback]::mouse_event(0x0010, 0, 0, 0, 0)
|
|
193
|
+
Write-Output "{\`"success\`":true,\`"action\`":\`"right_click\`",\`"button\`":\`"right\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
|
|
194
|
+
} elseif ($act -eq "double_click") {
|
|
195
|
+
$x = [int]${Number(args[0]) || 0}
|
|
196
|
+
$y = [int]${Number(args[1]) || 0}
|
|
197
|
+
[WinDriverFallback]::SetCursorPos($x, $y)
|
|
198
|
+
Start-Sleep -Milliseconds 30
|
|
199
|
+
[WinDriverFallback]::mouse_event(0x0002, 0, 0, 0, 0)
|
|
200
|
+
Start-Sleep -Milliseconds 40
|
|
201
|
+
[WinDriverFallback]::mouse_event(0x0004, 0, 0, 0, 0)
|
|
202
|
+
Start-Sleep -Milliseconds 80
|
|
203
|
+
[WinDriverFallback]::mouse_event(0x0002, 0, 0, 0, 0)
|
|
204
|
+
Start-Sleep -Milliseconds 40
|
|
205
|
+
[WinDriverFallback]::mouse_event(0x0004, 0, 0, 0, 0)
|
|
206
|
+
Write-Output "{\`"success\`":true,\`"action\`":\`"double_click\`",\`"coordinates\`":{\`"x\`":$x,\`"y\`":$y}}"
|
|
207
|
+
} elseif ($act -eq "screenshot") {
|
|
208
|
+
$w = [WinDriverFallback]::GetSystemMetrics(0)
|
|
209
|
+
$h = [WinDriverFallback]::GetSystemMetrics(1)
|
|
210
|
+
if ($w -le 0) { $w = 1920 }
|
|
211
|
+
if ($h -le 0) { $h = 1080 }
|
|
212
|
+
$p = "${(args[0] || '').replace(/\\/g, '\\\\').replace(/"/g, '`"')}"
|
|
213
|
+
$bmp = New-Object System.Drawing.Bitmap $w, $h
|
|
214
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
215
|
+
$g.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size($w, $h)), [System.Drawing.CopyPixelOperation]::SourceCopy)
|
|
216
|
+
$bmp.Save($p, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
217
|
+
$g.Dispose()
|
|
218
|
+
$bmp.Dispose()
|
|
219
|
+
Write-Output "{\`"success\`":true,\`"action\`":\`"screenshot\`",\`"width\`":$w,\`"height\`":$h,\`"path\`":\`"$p\`"}"
|
|
220
|
+
} else {
|
|
221
|
+
Write-Output "{\`"success\`":true,\`"action\`":\`"$act\`"}"
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
try {
|
|
225
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
226
|
+
const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`);
|
|
227
|
+
const trimmed = (stdout || '').trim();
|
|
228
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
229
|
+
return JSON.parse(trimmed);
|
|
230
|
+
}
|
|
231
|
+
return { success: true, output: trimmed };
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return { success: false, output: `PowerShell fallback failed: ${err?.message || String(err)}`, error: err?.message, recoverable: true };
|
|
235
|
+
}
|
|
31
236
|
}
|
|
32
237
|
async screenshot() {
|
|
33
238
|
const timestamp = new Date().toISOString();
|
|
34
239
|
const filename = `screen_${Date.now()}.png`;
|
|
35
240
|
const filePath = path.join(this.screenshotsDir, filename);
|
|
36
241
|
if (process.platform === 'win32') {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
42
|
-
$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
|
43
|
-
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
|
44
|
-
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
45
|
-
$bmp.Save('${filePath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
46
|
-
$graphics.Dispose()
|
|
47
|
-
$bmp.Dispose()
|
|
48
|
-
Write-Output "$($bounds.Width)x$($bounds.Height)"
|
|
49
|
-
`.trim();
|
|
50
|
-
const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
|
|
51
|
-
const dims = stdout.trim().split('x');
|
|
52
|
-
const width = parseInt(dims[0] || '1920', 10);
|
|
53
|
-
const height = parseInt(dims[1] || '1080', 10);
|
|
54
|
-
const ui = await this.inspectUI();
|
|
242
|
+
const res = await this.runDriverAction('screenshot', [filePath]);
|
|
243
|
+
if (res && res.success) {
|
|
244
|
+
const activeWin = res.activeWindow || (await this.inspectUI()).activeWindow;
|
|
245
|
+
this.activeWindow = activeWin;
|
|
55
246
|
return {
|
|
56
247
|
path: filePath,
|
|
57
|
-
width,
|
|
58
|
-
height,
|
|
59
|
-
activeWindow:
|
|
60
|
-
timestamp,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
return {
|
|
65
|
-
width: 1920,
|
|
66
|
-
height: 1080,
|
|
67
|
-
activeWindow: 'Desktop',
|
|
248
|
+
width: Number(res.width) || 1920,
|
|
249
|
+
height: Number(res.height) || 1080,
|
|
250
|
+
activeWindow: activeWin,
|
|
68
251
|
timestamp,
|
|
69
252
|
};
|
|
70
253
|
}
|
|
@@ -79,7 +262,6 @@ Write-Output "$($bounds.Width)x$($bounds.Height)"
|
|
|
79
262
|
}
|
|
80
263
|
}
|
|
81
264
|
else {
|
|
82
|
-
// Linux fallback (scrot or import)
|
|
83
265
|
try {
|
|
84
266
|
await execAsync(`scrot "${filePath}"`);
|
|
85
267
|
return { path: filePath, width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
|
|
@@ -88,258 +270,286 @@ Write-Output "$($bounds.Width)x$($bounds.Height)"
|
|
|
88
270
|
return { width: 1920, height: 1080, timestamp };
|
|
89
271
|
}
|
|
90
272
|
}
|
|
273
|
+
return { width: 1920, height: 1080, activeWindow: 'Desktop', timestamp };
|
|
91
274
|
}
|
|
92
275
|
async click(x, y, options) {
|
|
93
276
|
const btn = options?.button || 'left';
|
|
94
277
|
const count = options?.clicks || 1;
|
|
95
278
|
if (process.platform === 'win32') {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: `for ($i=0; $i -lt ${count}; $i++) { [WinMouse]::mouse_event(0x0002, 0, 0, 0, 0); Start-Sleep -Milliseconds 40; [WinMouse]::mouse_event(0x0004, 0, 0, 0, 0); Start-Sleep -Milliseconds 50 }`}
|
|
114
|
-
`.trim();
|
|
115
|
-
await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
|
|
116
|
-
return { success: true, output: `Clicked ${btn} button at (${x}, ${y})` };
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
return { success: false, output: `Click failed at (${x}, ${y}): ${err?.message || String(err)}` };
|
|
120
|
-
}
|
|
279
|
+
const act = count === 2 ? 'double_click' : btn === 'right' ? 'right_click' : 'click';
|
|
280
|
+
const res = await this.runDriverAction(act, [x, y, btn]);
|
|
281
|
+
const success = Boolean(res?.success);
|
|
282
|
+
const coords = res?.coordinates || { x, y };
|
|
283
|
+
const activeWin = res?.activeWindow || this.activeWindow || undefined;
|
|
284
|
+
return {
|
|
285
|
+
success,
|
|
286
|
+
output: success
|
|
287
|
+
? `Clicked ${btn} button at (${coords.x}, ${coords.y})`
|
|
288
|
+
: `Click failed at (${x}, ${y}): ${res?.error || 'Unknown error'}`,
|
|
289
|
+
action: 'click',
|
|
290
|
+
coordinates: coords,
|
|
291
|
+
button: btn,
|
|
292
|
+
error: res?.error,
|
|
293
|
+
recoverable: res?.recoverable ?? true,
|
|
294
|
+
activeWindow: activeWin,
|
|
295
|
+
};
|
|
121
296
|
}
|
|
122
|
-
return {
|
|
297
|
+
return {
|
|
298
|
+
success: true,
|
|
299
|
+
output: `Simulated click ${btn} at (${x}, ${y})`,
|
|
300
|
+
action: 'click',
|
|
301
|
+
coordinates: { x, y },
|
|
302
|
+
button: btn,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async leftClick(x, y) {
|
|
306
|
+
return this.click(x, y, { button: 'left', clicks: 1 });
|
|
307
|
+
}
|
|
308
|
+
async rightClick(x, y) {
|
|
309
|
+
return this.click(x, y, { button: 'right', clicks: 1 });
|
|
123
310
|
}
|
|
124
311
|
async doubleClick(x, y) {
|
|
125
|
-
|
|
312
|
+
if (process.platform === 'win32') {
|
|
313
|
+
const res = await this.runDriverAction('double_click', [x, y]);
|
|
314
|
+
const success = Boolean(res?.success);
|
|
315
|
+
const coords = res?.coordinates || { x, y };
|
|
316
|
+
return {
|
|
317
|
+
success,
|
|
318
|
+
output: success
|
|
319
|
+
? `Double-clicked at (${coords.x}, ${coords.y})`
|
|
320
|
+
: `Double-click failed at (${x}, ${y}): ${res?.error || 'Unknown error'}`,
|
|
321
|
+
action: 'double_click',
|
|
322
|
+
coordinates: coords,
|
|
323
|
+
button: 'left',
|
|
324
|
+
error: res?.error,
|
|
325
|
+
recoverable: res?.recoverable ?? true,
|
|
326
|
+
activeWindow: res?.activeWindow || this.activeWindow || undefined,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
success: true,
|
|
331
|
+
output: `Simulated double-click at (${x}, ${y})`,
|
|
332
|
+
action: 'double_click',
|
|
333
|
+
coordinates: { x, y },
|
|
334
|
+
button: 'left',
|
|
335
|
+
};
|
|
126
336
|
}
|
|
127
|
-
async
|
|
128
|
-
|
|
129
|
-
return { success: true, output: 'Typed empty string', action: 'type' };
|
|
337
|
+
async mouseDown(x, y, button) {
|
|
338
|
+
const btn = button || 'left';
|
|
130
339
|
if (process.platform === 'win32') {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.replace(/\[/g, '{[}')
|
|
143
|
-
.replace(/\]/g, '{]}');
|
|
144
|
-
const psScript = `
|
|
145
|
-
$wshell = New-Object -ComObject Wscript.Shell
|
|
146
|
-
$null = $wshell.AppActivate('${targetApp}')
|
|
147
|
-
Start-Sleep -Milliseconds 250
|
|
148
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
149
|
-
[System.Windows.Forms.SendKeys]::SendWait("${escaped.replace(/"/g, '`"')}")
|
|
150
|
-
`.trim();
|
|
151
|
-
await this.runPowerShell(psScript);
|
|
152
|
-
this.activeWindow = targetApp;
|
|
153
|
-
return {
|
|
154
|
-
success: true,
|
|
155
|
-
output: `Typed text (${text.length} chars) into active application "${targetApp}"`,
|
|
156
|
-
action: 'type',
|
|
157
|
-
activeWindow: targetApp,
|
|
158
|
-
expectedWindow: targetApp,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
return {
|
|
163
|
-
success: false,
|
|
164
|
-
output: `Type error on active window "${this.expectedApplication || 'foreground'}": ${err?.message || String(err)}`,
|
|
165
|
-
action: 'type',
|
|
166
|
-
error: err?.message || String(err),
|
|
167
|
-
recoverable: true,
|
|
168
|
-
expectedWindow: this.expectedApplication || undefined,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
340
|
+
const res = await this.runDriverAction('mouse_down', [x ?? -1, y ?? -1, btn]);
|
|
341
|
+
const coords = res?.coordinates || (x !== undefined && y !== undefined ? { x, y } : undefined);
|
|
342
|
+
return {
|
|
343
|
+
success: Boolean(res?.success),
|
|
344
|
+
output: `Mouse down (${btn}) at (${coords?.x ?? 'current'}, ${coords?.y ?? 'current'})`,
|
|
345
|
+
action: 'mouse_down',
|
|
346
|
+
button: btn,
|
|
347
|
+
coordinates: coords,
|
|
348
|
+
error: res?.error,
|
|
349
|
+
recoverable: true,
|
|
350
|
+
};
|
|
171
351
|
}
|
|
172
352
|
return {
|
|
173
353
|
success: true,
|
|
174
|
-
output: `Simulated
|
|
175
|
-
action: '
|
|
176
|
-
|
|
354
|
+
output: `Simulated mouse down (${btn})`,
|
|
355
|
+
action: 'mouse_down',
|
|
356
|
+
button: btn,
|
|
357
|
+
coordinates: x !== undefined && y !== undefined ? { x, y } : undefined,
|
|
177
358
|
};
|
|
178
359
|
}
|
|
179
|
-
async
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
360
|
+
async mouseUp(x, y, button) {
|
|
361
|
+
const btn = button || 'left';
|
|
362
|
+
if (process.platform === 'win32') {
|
|
363
|
+
const res = await this.runDriverAction('mouse_up', [x ?? -1, y ?? -1, btn]);
|
|
364
|
+
const coords = res?.coordinates || (x !== undefined && y !== undefined ? { x, y } : undefined);
|
|
365
|
+
return {
|
|
366
|
+
success: Boolean(res?.success),
|
|
367
|
+
output: `Mouse up (${btn}) at (${coords?.x ?? 'current'}, ${coords?.y ?? 'current'})`,
|
|
368
|
+
action: 'mouse_up',
|
|
369
|
+
button: btn,
|
|
370
|
+
coordinates: coords,
|
|
371
|
+
error: res?.error,
|
|
372
|
+
recoverable: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
success: true,
|
|
377
|
+
output: `Simulated mouse up (${btn})`,
|
|
378
|
+
action: 'mouse_up',
|
|
379
|
+
button: btn,
|
|
380
|
+
coordinates: x !== undefined && y !== undefined ? { x, y } : undefined,
|
|
197
381
|
};
|
|
198
|
-
|
|
382
|
+
}
|
|
383
|
+
async mouseMove(x, y) {
|
|
384
|
+
return this.move(x, y);
|
|
385
|
+
}
|
|
386
|
+
async move(x, y) {
|
|
199
387
|
if (process.platform === 'win32') {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
catch (err) {
|
|
217
|
-
return {
|
|
218
|
-
success: false,
|
|
219
|
-
output: `KeyPress error: ${err?.message || String(err)}`,
|
|
220
|
-
action: 'key_press',
|
|
221
|
-
error: err?.message || String(err),
|
|
222
|
-
recoverable: true,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
388
|
+
const res = await this.runDriverAction('move', [x, y]);
|
|
389
|
+
const success = Boolean(res?.success);
|
|
390
|
+
const coords = res?.coordinates || { x, y };
|
|
391
|
+
return {
|
|
392
|
+
success,
|
|
393
|
+
output: success
|
|
394
|
+
? `Moved mouse cursor to (${coords.x}, ${coords.y})`
|
|
395
|
+
: `MouseMove failed to (${x}, ${y}): ${res?.error || 'Unknown error'}`,
|
|
396
|
+
action: 'move',
|
|
397
|
+
coordinates: coords,
|
|
398
|
+
error: res?.error,
|
|
399
|
+
recoverable: res?.recoverable ?? true,
|
|
400
|
+
activeWindow: res?.activeWindow || this.activeWindow || undefined,
|
|
401
|
+
};
|
|
225
402
|
}
|
|
226
|
-
return {
|
|
403
|
+
return {
|
|
404
|
+
success: true,
|
|
405
|
+
output: `Simulated mouse move to (${x}, ${y})`,
|
|
406
|
+
action: 'move',
|
|
407
|
+
coordinates: { x, y },
|
|
408
|
+
};
|
|
227
409
|
}
|
|
228
|
-
async
|
|
229
|
-
if (!keys || keys.length === 0)
|
|
230
|
-
return { success: false, output: 'No keys provided for hotkey', action: 'hotkey' };
|
|
410
|
+
async drag(startX, startY, endX, endY) {
|
|
231
411
|
if (process.platform === 'win32') {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
const sendPattern = `${prefix}${mainKey.toLowerCase()}`;
|
|
248
|
-
const psScript = `
|
|
249
|
-
$wshell = New-Object -ComObject Wscript.Shell
|
|
250
|
-
${targetApp ? `$null = $wshell.AppActivate('${targetApp}')\nStart-Sleep -Milliseconds 150` : ''}
|
|
251
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
252
|
-
[System.Windows.Forms.SendKeys]::SendWait("${sendPattern}")
|
|
253
|
-
`.trim();
|
|
254
|
-
await this.runPowerShell(psScript);
|
|
255
|
-
return {
|
|
256
|
-
success: true,
|
|
257
|
-
output: `Executed hotkey: ${keys.join('+')} on "${targetApp || 'active window'}"`,
|
|
258
|
-
action: 'hotkey',
|
|
259
|
-
activeWindow: targetApp || undefined,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
return {
|
|
264
|
-
success: false,
|
|
265
|
-
output: `Hotkey error: ${err?.message || String(err)}`,
|
|
266
|
-
action: 'hotkey',
|
|
267
|
-
error: err?.message || String(err),
|
|
268
|
-
recoverable: true,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
412
|
+
const res = await this.runDriverAction('drag', [startX, startY, endX, endY]);
|
|
413
|
+
const success = Boolean(res?.success);
|
|
414
|
+
return {
|
|
415
|
+
success,
|
|
416
|
+
output: success
|
|
417
|
+
? `Dragged from (${startX}, ${startY}) to (${endX}, ${endY})`
|
|
418
|
+
: `Drag failed: ${res?.error || 'Unknown error'}`,
|
|
419
|
+
action: 'drag',
|
|
420
|
+
from: { x: startX, y: startY },
|
|
421
|
+
to: { x: endX, y: endY },
|
|
422
|
+
coordinates: res?.coordinates || { x: endX, y: endY },
|
|
423
|
+
error: res?.error,
|
|
424
|
+
recoverable: res?.recoverable ?? true,
|
|
425
|
+
};
|
|
271
426
|
}
|
|
272
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
output: `Simulated drag from (${startX}, ${startY}) to (${endX}, ${endY})`,
|
|
430
|
+
action: 'drag',
|
|
431
|
+
from: { x: startX, y: startY },
|
|
432
|
+
to: { x: endX, y: endY },
|
|
433
|
+
coordinates: { x: endX, y: endY },
|
|
434
|
+
};
|
|
273
435
|
}
|
|
274
436
|
async scroll(deltaX, deltaY) {
|
|
275
437
|
if (process.platform === 'win32') {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
|
|
284
|
-
}
|
|
285
|
-
"@
|
|
286
|
-
Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue
|
|
287
|
-
# MOUSEEVENTF_WHEEL = 0x0800
|
|
288
|
-
[WinMouseScroll]::mouse_event(0x0800, 0, 0, [uint32]${Math.round(-deltaY * 120)}, 0)
|
|
289
|
-
`.trim();
|
|
290
|
-
await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
|
|
291
|
-
return { success: true, output: `Scrolled deltaY=${deltaY}, deltaX=${deltaX}` };
|
|
292
|
-
}
|
|
293
|
-
catch (err) {
|
|
294
|
-
return { success: false, output: `Scroll error: ${err?.message || String(err)}` };
|
|
295
|
-
}
|
|
438
|
+
const res = await this.runDriverAction('scroll', [deltaX, deltaY]);
|
|
439
|
+
return {
|
|
440
|
+
success: Boolean(res?.success),
|
|
441
|
+
output: `Scrolled deltaY=${deltaY}, deltaX=${deltaX}`,
|
|
442
|
+
action: 'scroll',
|
|
443
|
+
details: { deltaX, deltaY },
|
|
444
|
+
};
|
|
296
445
|
}
|
|
297
|
-
return {
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
output: `Simulated scroll deltaY=${deltaY}, deltaX=${deltaX}`,
|
|
449
|
+
action: 'scroll',
|
|
450
|
+
details: { deltaX, deltaY },
|
|
451
|
+
};
|
|
298
452
|
}
|
|
299
|
-
async
|
|
453
|
+
async getMousePosition() {
|
|
300
454
|
if (process.platform === 'win32') {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(x)}, ${Math.round(y)})
|
|
305
|
-
`.trim();
|
|
306
|
-
await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
|
|
307
|
-
return { success: true, output: `Moved mouse cursor to (${x}, ${y})` };
|
|
455
|
+
const res = await this.runDriverAction('position', []);
|
|
456
|
+
if (res && res.coordinates) {
|
|
457
|
+
return res.coordinates;
|
|
308
458
|
}
|
|
309
|
-
|
|
310
|
-
|
|
459
|
+
}
|
|
460
|
+
return { x: 0, y: 0 };
|
|
461
|
+
}
|
|
462
|
+
async runDiagnostic() {
|
|
463
|
+
if (process.platform === 'win32') {
|
|
464
|
+
const res = await this.runDriverAction('diagnostic', []);
|
|
465
|
+
return {
|
|
466
|
+
success: Boolean(res?.success),
|
|
467
|
+
output: `Diagnostic: Screen ${res?.width}x${res?.height}, Mouse (${res?.mouse?.x}, ${res?.mouse?.y}), Active: "${res?.activeWindow}", Windows: ${res?.windowCount}`,
|
|
468
|
+
action: 'diagnostic',
|
|
469
|
+
coordinates: res?.mouse,
|
|
470
|
+
activeWindow: res?.activeWindow,
|
|
471
|
+
details: res,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
success: true,
|
|
476
|
+
output: `Diagnostic on ${process.platform}: Screen 1920x1080, Mouse (0, 0), Active: "Desktop"`,
|
|
477
|
+
action: 'diagnostic',
|
|
478
|
+
coordinates: { x: 0, y: 0 },
|
|
479
|
+
activeWindow: 'Desktop',
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
async type(text) {
|
|
483
|
+
if (!text)
|
|
484
|
+
return { success: true, output: 'Typed empty string', action: 'type' };
|
|
485
|
+
if (process.platform === 'win32') {
|
|
486
|
+
const targetApp = this.expectedApplication || '';
|
|
487
|
+
if (targetApp) {
|
|
488
|
+
await this.switchWindow(targetApp);
|
|
311
489
|
}
|
|
490
|
+
const res = await this.runDriverAction('type', [text]);
|
|
491
|
+
const success = Boolean(res?.success);
|
|
492
|
+
const activeWin = res?.activeWindow || targetApp || 'foreground';
|
|
493
|
+
this.activeWindow = activeWin;
|
|
494
|
+
return {
|
|
495
|
+
success,
|
|
496
|
+
output: success
|
|
497
|
+
? `Typed text (${text.length} chars) into "${activeWin}"`
|
|
498
|
+
: `Type failed: ${res?.error || 'Unknown error'}`,
|
|
499
|
+
action: 'type',
|
|
500
|
+
error: res?.error,
|
|
501
|
+
recoverable: true,
|
|
502
|
+
activeWindow: activeWin,
|
|
503
|
+
expectedWindow: this.expectedApplication || undefined,
|
|
504
|
+
};
|
|
312
505
|
}
|
|
313
|
-
return {
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
output: `Simulated type (${text.length} chars)`,
|
|
509
|
+
action: 'type',
|
|
510
|
+
activeWindow: this.expectedApplication || 'Simulated Window',
|
|
511
|
+
};
|
|
314
512
|
}
|
|
315
|
-
async
|
|
316
|
-
await this.mouseMove(fromX, fromY);
|
|
513
|
+
async keyPress(key) {
|
|
317
514
|
if (process.platform === 'win32') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
$src = @"
|
|
322
|
-
using System;
|
|
323
|
-
using System.Runtime.InteropServices;
|
|
324
|
-
public class WinMouseDrag {
|
|
325
|
-
[DllImport("user32.dll")]
|
|
326
|
-
public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
|
|
327
|
-
}
|
|
328
|
-
"@
|
|
329
|
-
Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue
|
|
330
|
-
[WinMouseDrag]::mouse_event(0x0002, 0, 0, 0, 0)
|
|
331
|
-
[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${Math.round(toX)}, ${Math.round(toY)})
|
|
332
|
-
Start-Sleep -Milliseconds 100
|
|
333
|
-
[WinMouseDrag]::mouse_event(0x0004, 0, 0, 0, 0)
|
|
334
|
-
`.trim();
|
|
335
|
-
await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
|
|
336
|
-
return { success: true, output: `Dragged from (${fromX}, ${fromY}) to (${toX}, ${toY})` };
|
|
515
|
+
const targetApp = this.expectedApplication || '';
|
|
516
|
+
if (targetApp) {
|
|
517
|
+
await this.switchWindow(targetApp);
|
|
337
518
|
}
|
|
338
|
-
|
|
339
|
-
|
|
519
|
+
const res = await this.runDriverAction('key', [key]);
|
|
520
|
+
const success = Boolean(res?.success);
|
|
521
|
+
return {
|
|
522
|
+
success,
|
|
523
|
+
output: success ? `Pressed key: ${key}` : `KeyPress failed: ${res?.error || 'Unknown error'}`,
|
|
524
|
+
action: 'key_press',
|
|
525
|
+
error: res?.error,
|
|
526
|
+
recoverable: true,
|
|
527
|
+
activeWindow: res?.activeWindow || targetApp || undefined,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return { success: true, output: `Simulated key press: ${key}`, action: 'key_press' };
|
|
531
|
+
}
|
|
532
|
+
async hotkey(keys) {
|
|
533
|
+
if (!keys || keys.length === 0) {
|
|
534
|
+
return { success: false, output: 'No keys provided for hotkey', action: 'hotkey', error: 'No keys provided' };
|
|
535
|
+
}
|
|
536
|
+
if (process.platform === 'win32') {
|
|
537
|
+
const targetApp = this.expectedApplication || '';
|
|
538
|
+
if (targetApp) {
|
|
539
|
+
await this.switchWindow(targetApp);
|
|
340
540
|
}
|
|
541
|
+
const res = await this.runDriverAction('hotkey', [keys.join('+')]);
|
|
542
|
+
const success = Boolean(res?.success);
|
|
543
|
+
return {
|
|
544
|
+
success,
|
|
545
|
+
output: success ? `Executed hotkey: ${keys.join('+')}` : `Hotkey failed: ${res?.error || 'Unknown error'}`,
|
|
546
|
+
action: 'hotkey',
|
|
547
|
+
error: res?.error,
|
|
548
|
+
recoverable: true,
|
|
549
|
+
activeWindow: res?.activeWindow || targetApp || undefined,
|
|
550
|
+
};
|
|
341
551
|
}
|
|
342
|
-
return { success: true, output: `Simulated
|
|
552
|
+
return { success: true, output: `Simulated hotkey: ${keys.join('+')}`, action: 'hotkey' };
|
|
343
553
|
}
|
|
344
554
|
async wait(ms) {
|
|
345
555
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
@@ -349,27 +559,35 @@ Start-Sleep -Milliseconds 100
|
|
|
349
559
|
const argsStr = args.join(' ');
|
|
350
560
|
try {
|
|
351
561
|
if (process.platform === 'win32') {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
562
|
+
const driverRes = await this.runDriverAction('launch', [appName]);
|
|
563
|
+
if (!driverRes || !driverRes.success) {
|
|
564
|
+
if (appLower === 'code' || appLower === 'vscode') {
|
|
565
|
+
await execAsync(`start code ${argsStr}`);
|
|
566
|
+
}
|
|
567
|
+
else if (appLower === 'explorer' || appLower === 'file explorer') {
|
|
568
|
+
await execAsync(`start explorer ${argsStr || '.'}`);
|
|
569
|
+
}
|
|
570
|
+
else if (appLower === 'notepad') {
|
|
571
|
+
await execAsync(`start notepad ${argsStr}`);
|
|
572
|
+
}
|
|
573
|
+
else if (appLower === 'terminal' || appLower === 'cmd' || appLower === 'powershell') {
|
|
574
|
+
await execAsync(`start powershell ${argsStr}`);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
await execAsync(`start "" "${appName}" ${argsStr}`);
|
|
578
|
+
}
|
|
355
579
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
this.expectedApplication = 'PowerShell';
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
await execAsync(`start "" "${appName}" ${argsStr}`);
|
|
370
|
-
this.expectedApplication = appName;
|
|
580
|
+
this.expectedApplication = appName;
|
|
581
|
+
// Observation-based wait: poll inspectUI to confirm window appeared
|
|
582
|
+
const delays = [400, 800, 1200];
|
|
583
|
+
for (const delay of delays) {
|
|
584
|
+
await this.wait(delay);
|
|
585
|
+
const ui = await this.inspectUI();
|
|
586
|
+
const match = ui.windows.some(w => w.toLowerCase().includes(appName.toLowerCase()));
|
|
587
|
+
if (match || (ui.activeWindow && ui.activeWindow.toLowerCase().includes(appName.toLowerCase()))) {
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
371
590
|
}
|
|
372
|
-
await this.wait(350);
|
|
373
591
|
if (this.expectedApplication) {
|
|
374
592
|
await this.switchWindow(this.expectedApplication);
|
|
375
593
|
}
|
|
@@ -432,8 +650,21 @@ Start-Sleep -Milliseconds 100
|
|
|
432
650
|
}
|
|
433
651
|
async switchWindow(windowTitleOrApp) {
|
|
434
652
|
if (process.platform === 'win32') {
|
|
653
|
+
const res = await this.runDriverAction('focus_window', [windowTitleOrApp]);
|
|
654
|
+
if (res && res.success) {
|
|
655
|
+
this.activeWindow = res.activeWindow || windowTitleOrApp;
|
|
656
|
+
this.expectedApplication = windowTitleOrApp;
|
|
657
|
+
return {
|
|
658
|
+
success: true,
|
|
659
|
+
output: `Focused window matching: "${windowTitleOrApp}"`,
|
|
660
|
+
action: 'switch_window',
|
|
661
|
+
activeWindow: this.activeWindow || undefined,
|
|
662
|
+
expectedWindow: windowTitleOrApp,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
// Fallback to WScript.Shell AppActivate
|
|
435
666
|
try {
|
|
436
|
-
const
|
|
667
|
+
const script = `
|
|
437
668
|
$wshell = New-Object -ComObject Wscript.Shell
|
|
438
669
|
$result = $wshell.AppActivate("${windowTitleOrApp.replace(/"/g, '`"')}")
|
|
439
670
|
if (-not $result) {
|
|
@@ -443,13 +674,13 @@ if (-not $result) {
|
|
|
443
674
|
$result = $true
|
|
444
675
|
}
|
|
445
676
|
}
|
|
446
|
-
Start-Sleep -Milliseconds 150
|
|
447
677
|
if ($result) { Write-Output "OK" } else { Write-Output "NOT_FOUND" }
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
this.expectedApplication = windowTitleOrApp;
|
|
678
|
+
`;
|
|
679
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
680
|
+
const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`);
|
|
452
681
|
if (stdout.includes('OK')) {
|
|
682
|
+
this.activeWindow = windowTitleOrApp;
|
|
683
|
+
this.expectedApplication = windowTitleOrApp;
|
|
453
684
|
return {
|
|
454
685
|
success: true,
|
|
455
686
|
output: `Focused window matching: "${windowTitleOrApp}"`,
|
|
@@ -458,33 +689,38 @@ if ($result) { Write-Output "OK" } else { Write-Output "NOT_FOUND" }
|
|
|
458
689
|
expectedWindow: windowTitleOrApp,
|
|
459
690
|
};
|
|
460
691
|
}
|
|
461
|
-
return {
|
|
462
|
-
success: false,
|
|
463
|
-
output: `Could not find or focus window: "${windowTitleOrApp}". Try computer_inspect_ui to list open windows.`,
|
|
464
|
-
action: 'switch_window',
|
|
465
|
-
recoverable: true,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
catch (err) {
|
|
469
|
-
return {
|
|
470
|
-
success: false,
|
|
471
|
-
output: `Window switch error for "${windowTitleOrApp}": ${err?.message || String(err)}`,
|
|
472
|
-
action: 'switch_window',
|
|
473
|
-
error: err?.message || String(err),
|
|
474
|
-
recoverable: true,
|
|
475
|
-
};
|
|
476
692
|
}
|
|
693
|
+
catch { }
|
|
694
|
+
return {
|
|
695
|
+
success: false,
|
|
696
|
+
output: `Could not find or focus window: "${windowTitleOrApp}". Try computer_inspect_ui to list open windows.`,
|
|
697
|
+
action: 'switch_window',
|
|
698
|
+
recoverable: true,
|
|
699
|
+
};
|
|
477
700
|
}
|
|
478
701
|
this.activeWindow = windowTitleOrApp;
|
|
479
702
|
return { success: true, output: `Switched window to: "${windowTitleOrApp}"`, action: 'switch_window' };
|
|
480
703
|
}
|
|
704
|
+
async getWindowBounds(windowTitleOrApp) {
|
|
705
|
+
if (process.platform === 'win32') {
|
|
706
|
+
const res = await this.runDriverAction('window_bounds', [windowTitleOrApp]);
|
|
707
|
+
if (res && res.success && typeof res.x === 'number') {
|
|
708
|
+
return { x: res.x, y: res.y, width: res.width, height: res.height };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
481
713
|
async inspectUI() {
|
|
482
714
|
if (process.platform === 'win32') {
|
|
715
|
+
const res = await this.runDriverAction('list_windows', []);
|
|
716
|
+
if (res && res.success && Array.isArray(res.windows)) {
|
|
717
|
+
return {
|
|
718
|
+
activeWindow: res.activeWindow || 'Desktop',
|
|
719
|
+
windows: res.windows,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
483
722
|
try {
|
|
484
|
-
const
|
|
485
|
-
Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle.Trim().Length -gt 0 } | Select-Object -ExpandProperty MainWindowTitle
|
|
486
|
-
`.trim();
|
|
487
|
-
const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, '; ')}"`);
|
|
723
|
+
const { stdout } = await execAsync(`powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle.Trim().Length -gt 0 } | Select-Object -ExpandProperty MainWindowTitle"`);
|
|
488
724
|
const titles = stdout
|
|
489
725
|
.split('\n')
|
|
490
726
|
.map((t) => t.trim())
|