@lardexx/arenacode 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/package.json +8 -4
- package/src/automation/ffi.js +62 -0
- package/src/automation/linux.js +809 -0
- package/src/automation/win-dll.js +493 -0
- package/src/automation.js +430 -92
- package/src/local.js +6 -17
- package/vendor/windows-use/windows-use.dll +0 -0
package/src/automation.js
CHANGED
|
@@ -55,6 +55,8 @@ $ErrorActionPreference = 'Stop'
|
|
|
55
55
|
$req = ([Console]::In.ReadToEnd() | ConvertFrom-Json)
|
|
56
56
|
Add-Type -AssemblyName System.Drawing
|
|
57
57
|
Add-Type -AssemblyName System.Windows.Forms
|
|
58
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
59
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
58
60
|
Add-Type -TypeDefinition @'
|
|
59
61
|
using System;
|
|
60
62
|
using System.Text;
|
|
@@ -67,48 +69,66 @@ public class AC {
|
|
|
67
69
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
68
70
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
|
69
71
|
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int ht, bool repaint);
|
|
70
|
-
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
|
|
71
|
-
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
72
|
-
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
73
|
-
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
74
72
|
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
73
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
75
74
|
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
|
76
|
-
public delegate bool EnumProc(IntPtr h, IntPtr p);
|
|
77
75
|
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
|
78
76
|
}
|
|
79
77
|
'@
|
|
80
78
|
|
|
79
|
+
$AC_TYPES = @(
|
|
80
|
+
[System.Windows.Automation.ControlType]::Button,
|
|
81
|
+
[System.Windows.Automation.ControlType]::CheckBox,
|
|
82
|
+
[System.Windows.Automation.ControlType]::ComboBox,
|
|
83
|
+
[System.Windows.Automation.ControlType]::Custom,
|
|
84
|
+
[System.Windows.Automation.ControlType]::DataItem,
|
|
85
|
+
[System.Windows.Automation.ControlType]::Edit,
|
|
86
|
+
[System.Windows.Automation.ControlType]::Hyperlink,
|
|
87
|
+
[System.Windows.Automation.ControlType]::ListItem,
|
|
88
|
+
[System.Windows.Automation.ControlType]::MenuItem,
|
|
89
|
+
[System.Windows.Automation.ControlType]::RadioButton,
|
|
90
|
+
[System.Windows.Automation.ControlType]::ScrollBar,
|
|
91
|
+
[System.Windows.Automation.ControlType]::Slider,
|
|
92
|
+
[System.Windows.Automation.ControlType]::Spinner,
|
|
93
|
+
[System.Windows.Automation.ControlType]::SplitButton,
|
|
94
|
+
[System.Windows.Automation.ControlType]::Tab,
|
|
95
|
+
[System.Windows.Automation.ControlType]::TabItem,
|
|
96
|
+
[System.Windows.Automation.ControlType]::Text,
|
|
97
|
+
[System.Windows.Automation.ControlType]::ToolBar,
|
|
98
|
+
[System.Windows.Automation.ControlType]::TreeItem
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
function Get-ACWindow($h) {
|
|
102
|
+
$hh = [IntPtr][int64]$h
|
|
103
|
+
$r = New-Object AC+RECT
|
|
104
|
+
[void][AC]::GetWindowRect($hh, [ref]$r)
|
|
105
|
+
$sb = New-Object System.Text.StringBuilder 1024
|
|
106
|
+
[void][AC]::GetWindowText($hh, $sb, 1024)
|
|
107
|
+
return [pscustomobject]@{
|
|
108
|
+
name = $sb.ToString()
|
|
109
|
+
handle = $hh.ToInt64()
|
|
110
|
+
hwnd = $hh.ToInt64()
|
|
111
|
+
x = $r.Left; y = $r.Top
|
|
112
|
+
width = [Math]::Max(0, $r.Right - $r.Left); height = [Math]::Max(0, $r.Bottom - $r.Top)
|
|
113
|
+
status = $(if ([AC]::IsIconic($hh)) { 'minimized' } else { 'normal' })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
function Get-ACWindows {
|
|
82
118
|
$list = New-Object System.Collections.ArrayList
|
|
83
119
|
foreach ($p in (Get-Process | Where-Object { $_.MainWindowHandle -ne 0 })) {
|
|
84
120
|
try {
|
|
85
|
-
$h = [IntPtr]$p.MainWindowHandle
|
|
86
|
-
$r = New-Object AC+RECT
|
|
87
|
-
[void][AC]::GetWindowRect($h, [ref]$r)
|
|
88
121
|
$title = [string]$p.MainWindowTitle
|
|
89
122
|
if ($title.Trim().Length -eq 0) { continue }
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
x = $r.Left; y = $r.Top
|
|
95
|
-
width = [Math]::Max(0, $r.Right - $r.Left); height = [Math]::Max(0, $r.Bottom - $r.Top)
|
|
96
|
-
process_id = [int]$p.Id
|
|
97
|
-
status = $(if ($p.MainWindowHandle -eq 0) { 'hidden' } else { 'normal' })
|
|
98
|
-
})
|
|
123
|
+
$w = Get-ACWindow $p.MainWindowHandle
|
|
124
|
+
$w | Add-Member -NotePropertyName process_id -NotePropertyValue ([int]$p.Id)
|
|
125
|
+
$w | Add-Member -NotePropertyName app -NotePropertyValue ([string]$p.ProcessName)
|
|
126
|
+
[void]$list.Add($w)
|
|
99
127
|
} catch { }
|
|
100
128
|
}
|
|
101
129
|
return $list
|
|
102
130
|
}
|
|
103
131
|
|
|
104
|
-
function Find-ACWindow($name) {
|
|
105
|
-
if (-not $name) { return $null }
|
|
106
|
-
$all = Get-ACWindows
|
|
107
|
-
$exact = $all | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
|
108
|
-
if ($exact) { return $exact }
|
|
109
|
-
return ($all | Where-Object { $_.name -like "*$name*" } | Select-Object -First 1)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
132
|
function Get-ACScreen {
|
|
113
133
|
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
114
134
|
return @{ width = $b.Width; height = $b.Height }
|
|
@@ -116,37 +136,119 @@ function Get-ACScreen {
|
|
|
116
136
|
|
|
117
137
|
function Send-ACKeys($pairs) {
|
|
118
138
|
foreach ($k in $pairs) {
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
$flags = 0
|
|
140
|
+
if ($k.flags -eq 2) { $flags = $flags -bor 2 }
|
|
141
|
+
if ($k.ext) { $flags = $flags -bor 1 }
|
|
142
|
+
[AC]::keybd_event([byte]$k.vk, 0, $flags, [UIntPtr]::Zero)
|
|
121
143
|
Start-Sleep -Milliseconds 12
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
146
|
|
|
147
|
+
# windows-use types through SendInput unicode events: the clipboard is never touched
|
|
148
|
+
function Send-ACUnicode($text) {
|
|
149
|
+
$typed = 0
|
|
150
|
+
foreach ($ch in $text.ToCharArray()) {
|
|
151
|
+
if ($ch -eq [char]10) {
|
|
152
|
+
[AC]::keybd_event(0x0D, 0, 0, [UIntPtr]::Zero)
|
|
153
|
+
[AC]::keybd_event(0x0D, 0, 2, [UIntPtr]::Zero)
|
|
154
|
+
$typed++
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if ($ch -eq [char]13) { continue }
|
|
158
|
+
[AC]::keybd_event(0, [byte][char]$ch, 4, [UIntPtr]::Zero)
|
|
159
|
+
[AC]::keybd_event(0, [byte][char]$ch, 6, [UIntPtr]::Zero)
|
|
160
|
+
$typed++
|
|
161
|
+
Start-Sleep -Milliseconds 3
|
|
162
|
+
}
|
|
163
|
+
return $typed
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function Get-ACElements($hwnd, $interactiveOnly) {
|
|
167
|
+
$handle = [IntPtr][int64]$hwnd
|
|
168
|
+
$root = $null
|
|
169
|
+
if ($handle -ne [IntPtr]::Zero) {
|
|
170
|
+
$root = [System.Windows.Automation.AutomationElement]::FromHandle($handle)
|
|
171
|
+
} else {
|
|
172
|
+
$root = [System.Windows.Automation.AutomationElement]::FocusedElement
|
|
173
|
+
while ($root -ne $null -and $root.Current.NativeWindowHandle -eq 0) {
|
|
174
|
+
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
|
175
|
+
$root = $walker.GetParent($root)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if ($root -eq $null) { return @() }
|
|
179
|
+
|
|
180
|
+
$conditions = @()
|
|
181
|
+
foreach ($ct in $AC_TYPES) {
|
|
182
|
+
$conditions += New-Object System.Windows.Automation.PropertyCondition(
|
|
183
|
+
[System.Windows.Automation.AutomationElement]::ControlTypeProperty, $ct)
|
|
184
|
+
}
|
|
185
|
+
$cond = New-Object System.Windows.Automation.OrCondition $conditions
|
|
186
|
+
$found = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $cond)
|
|
187
|
+
|
|
188
|
+
$out = New-Object System.Collections.ArrayList
|
|
189
|
+
$n = 0
|
|
190
|
+
foreach ($el in $found) {
|
|
191
|
+
if ($n -ge 120) { break }
|
|
192
|
+
try {
|
|
193
|
+
$cur = $el.Current
|
|
194
|
+
if ($cur.IsOffscreen) { continue }
|
|
195
|
+
$rect = $cur.BoundingRectangle
|
|
196
|
+
if ($rect.Width -le 0 -or $rect.Height -le 0) { continue }
|
|
197
|
+
$name = [string]$cur.Name
|
|
198
|
+
$ctype = [string]$cur.ControlType.ProgrammaticName -replace '^ControlType\.', ''
|
|
199
|
+
if ($interactiveOnly -and $ctype -eq 'Text' -and $name.Trim().Length -eq 0) { continue }
|
|
200
|
+
$val = ''
|
|
201
|
+
try {
|
|
202
|
+
$vp = $null
|
|
203
|
+
if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) { $val = [string]$vp.Current.Value }
|
|
204
|
+
} catch { $val = '' }
|
|
205
|
+
$scrollable = $false
|
|
206
|
+
try {
|
|
207
|
+
$sp = $null
|
|
208
|
+
if ($el.TryGetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern, [ref]$sp)) {
|
|
209
|
+
$scrollable = [bool]$sp.Current.VerticallyScrollable -or [bool]$sp.Current.HorizontallyScrollable
|
|
210
|
+
}
|
|
211
|
+
} catch { }
|
|
212
|
+
[void]$out.Add([pscustomobject]@{
|
|
213
|
+
name = $name
|
|
214
|
+
control_type = $ctype
|
|
215
|
+
kind = $ctype
|
|
216
|
+
bounding_box = @{ left = [int]$rect.X; top = [int]$rect.Y; right = [int]($rect.X + $rect.Width); bottom = [int]($rect.Y + $rect.Height) }
|
|
217
|
+
center = @{ x = [int]($rect.X + $rect.Width / 2); y = [int]($rect.Y + $rect.Height / 2) }
|
|
218
|
+
value = $val
|
|
219
|
+
scrollable = $scrollable
|
|
220
|
+
enabled = [bool]$cur.IsEnabled
|
|
221
|
+
automation_id = [string]$cur.AutomationId
|
|
222
|
+
})
|
|
223
|
+
$n++
|
|
224
|
+
} catch { }
|
|
225
|
+
}
|
|
226
|
+
return $out
|
|
227
|
+
}
|
|
228
|
+
|
|
125
229
|
$out = @{ ok = $true }
|
|
126
230
|
switch ($req.action) {
|
|
127
231
|
'screen' { $s = Get-ACScreen; $out.width = $s.width; $out.height = $s.height }
|
|
128
|
-
'windows' { $out.windows = @(Get-ACWindows)
|
|
232
|
+
'windows' { $out.windows = @(Get-ACWindows) }
|
|
129
233
|
'active' {
|
|
130
|
-
$
|
|
131
|
-
$
|
|
132
|
-
|
|
133
|
-
$out.
|
|
234
|
+
$hh = [AC]::GetForegroundWindow()
|
|
235
|
+
$w = Get-ACWindow $hh.ToInt64()
|
|
236
|
+
$out.name = $w.name
|
|
237
|
+
$out.handle = $w.handle
|
|
134
238
|
}
|
|
135
239
|
'focus' {
|
|
136
|
-
$
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
} else { $out = @{ ok = $false; error = "window not found: $($req.name)" } }
|
|
240
|
+
$hh = [IntPtr][int64]$req.handle
|
|
241
|
+
[void][AC]::ShowWindow($hh, 9)
|
|
242
|
+
[void][AC]::SetForegroundWindow($hh)
|
|
243
|
+
Start-Sleep -Milliseconds 200
|
|
244
|
+
$w = Get-ACWindow $req.handle
|
|
245
|
+
$out.name = $w.name
|
|
143
246
|
}
|
|
144
247
|
'resize' {
|
|
145
|
-
$
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} else { $out = @{ ok = $false; error = "window not found: $($req.name)" } }
|
|
248
|
+
$hh = [IntPtr][int64]$req.handle
|
|
249
|
+
[void][AC]::MoveWindow($hh, [int]$req.x, [int]$req.y, [int]$req.width, [int]$req.height, $true)
|
|
250
|
+
$w = Get-ACWindow $req.handle
|
|
251
|
+
$out.name = $w.name
|
|
150
252
|
}
|
|
151
253
|
'launch' {
|
|
152
254
|
$name = [string]$req.name
|
|
@@ -161,7 +263,7 @@ switch ($req.action) {
|
|
|
161
263
|
if ($exe) { Start-Process $exe.Source; $done = $true }
|
|
162
264
|
}
|
|
163
265
|
}
|
|
164
|
-
if ($done) { Start-Sleep -Milliseconds
|
|
266
|
+
if ($done) { Start-Sleep -Milliseconds 900; $out.name = $name } else { $out = @{ ok = $false; error = "app not found: $name" } }
|
|
165
267
|
}
|
|
166
268
|
'move' { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
|
|
167
269
|
'click' {
|
|
@@ -185,7 +287,7 @@ switch ($req.action) {
|
|
|
185
287
|
}
|
|
186
288
|
'up' {
|
|
187
289
|
$flag = 4; if ($req.button -eq 'right') { $flag = 16 } elseif ($req.button -eq 'middle') { $flag = 64 }
|
|
188
|
-
if ($
|
|
290
|
+
if ($null -ne $req.x) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
|
|
189
291
|
[AC]::mouse_event($flag, 0, 0, 0, [UIntPtr]::Zero)
|
|
190
292
|
}
|
|
191
293
|
'drag' {
|
|
@@ -202,7 +304,7 @@ switch ($req.action) {
|
|
|
202
304
|
[AC]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)
|
|
203
305
|
}
|
|
204
306
|
'scroll' {
|
|
205
|
-
if ($req.x -ne $
|
|
307
|
+
if ($null -ne $req.x -and $null -ne $req.y) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y); Start-Sleep -Milliseconds 40 }
|
|
206
308
|
$amount = [int]$req.amount
|
|
207
309
|
$dir = 120; if ($amount -lt 0) { $dir = -120 }
|
|
208
310
|
for ($i = 0; $i -lt [Math]::Abs($amount); $i++) {
|
|
@@ -212,24 +314,38 @@ switch ($req.action) {
|
|
|
212
314
|
}
|
|
213
315
|
'keys' { Send-ACKeys $req.sequence }
|
|
214
316
|
'type' {
|
|
215
|
-
if ($req.x -ne $
|
|
317
|
+
if ($null -ne $req.x -and $null -ne $req.y) {
|
|
216
318
|
[void][AC]::SetCursorPos([int]$req.x, [int]$req.y)
|
|
217
319
|
Start-Sleep -Milliseconds 60
|
|
218
320
|
[AC]::mouse_event(2, 0, 0, 0, [UIntPtr]::Zero)
|
|
219
321
|
[AC]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)
|
|
220
|
-
Start-Sleep -Milliseconds
|
|
322
|
+
Start-Sleep -Milliseconds 150
|
|
221
323
|
}
|
|
222
|
-
|
|
223
|
-
Start-Sleep -Milliseconds 80
|
|
224
|
-
[System.Windows.Forms.SendKeys]::SendWait('^v')
|
|
225
|
-
Start-Sleep -Milliseconds 150
|
|
226
|
-
$out.typed = ([string]$req.text).Length
|
|
324
|
+
$out.typed = (Send-ACUnicode ([string]$req.text))
|
|
227
325
|
}
|
|
228
326
|
'shot' {
|
|
229
327
|
$s = Get-ACScreen
|
|
230
328
|
$bmp = New-Object System.Drawing.Bitmap $s.width, $s.height
|
|
231
329
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
232
330
|
$g.CopyFromScreen(0, 0, 0, 0, $bmp.Size)
|
|
331
|
+
if ($req.annotate) {
|
|
332
|
+
$fg = $null
|
|
333
|
+
try { $fg = [AC]::GetForegroundWindow().ToInt64() } catch { $fg = 0 }
|
|
334
|
+
$els = @(Get-ACElements $fg $true)
|
|
335
|
+
$font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
|
|
336
|
+
$brush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(230, 250, 178, 131))
|
|
337
|
+
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(200, 250, 178, 131)), 1
|
|
338
|
+
$i = 1
|
|
339
|
+
foreach ($e in $els) {
|
|
340
|
+
$bb = $e.bounding_box
|
|
341
|
+
$g.DrawRectangle($pen, $bb.left, $bb.top, [Math]::Max(1, $bb.right - $bb.left), [Math]::Max(1, $bb.bottom - $bb.top))
|
|
342
|
+
$g.FillRectangle($brush, $bb.left, [Math]::Max(0, $bb.top - 14), 18, 14)
|
|
343
|
+
$g.DrawString([string]$i, $font, (New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::Black)), $bb.left + 2, [Math]::Max(0, $bb.top - 14))
|
|
344
|
+
$i++
|
|
345
|
+
}
|
|
346
|
+
$pen.Dispose(); $brush.Dispose(); $font.Dispose()
|
|
347
|
+
$out.elements = $els.Count
|
|
348
|
+
}
|
|
233
349
|
$bmp.Save($req.path, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
234
350
|
$g.Dispose(); $bmp.Dispose()
|
|
235
351
|
$out.width = $s.width; $out.height = $s.height; $out.file = $req.path
|
|
@@ -238,6 +354,35 @@ switch ($req.action) {
|
|
|
238
354
|
try { $out.apps = @(Get-StartApps | ForEach-Object { @{ name = $_.Name; app_id = $_.AppID } }) }
|
|
239
355
|
catch { $out.apps = @() }
|
|
240
356
|
}
|
|
357
|
+
'state' {
|
|
358
|
+
$hh = $null
|
|
359
|
+
if ($null -ne $req.handle -and [int64]$req.handle -ne 0) { $hh = [int64]$req.handle } else { $hh = [AC]::GetForegroundWindow().ToInt64() }
|
|
360
|
+
$els = @(Get-ACElements $hh $true)
|
|
361
|
+
$interactive = @()
|
|
362
|
+
$scrollable = @()
|
|
363
|
+
$lines = New-Object System.Collections.ArrayList
|
|
364
|
+
$i = 1
|
|
365
|
+
foreach ($e in $els) {
|
|
366
|
+
if ($e.scrollable) { $scrollable += $e } else { $interactive += $e }
|
|
367
|
+
[void]$lines.Add("$i. $($e.control_type) '$($e.name)' at ($($e.center.x), $($e.center.y))")
|
|
368
|
+
$i++
|
|
369
|
+
}
|
|
370
|
+
$out.windows = @(Get-ACWindows)
|
|
371
|
+
$out.active_window = (Get-ACWindow $hh)
|
|
372
|
+
$out.interactive_elements = $interactive
|
|
373
|
+
$out.scrollable_elements = $scrollable
|
|
374
|
+
$out.interactive_elements_text = ($lines -join [Environment]::NewLine)
|
|
375
|
+
$out.scrollable_elements_text = (($scrollable | ForEach-Object { "$($_.control_type) '$($_.name)' at ($($_.center.x), $($_.center.y))" }) -join [Environment]::NewLine)
|
|
376
|
+
$out.system = @{
|
|
377
|
+
windows_version = [string](Get-CimInstance Win32_OperatingSystem).Caption
|
|
378
|
+
os_version = [Environment]::OSVersion.Version.ToString()
|
|
379
|
+
screen_size = (Get-ACScreen)
|
|
380
|
+
user_account_type = 'Local Account'
|
|
381
|
+
default_language = (Get-Culture).Name
|
|
382
|
+
dpi_scaling = 1
|
|
383
|
+
}
|
|
384
|
+
$out.metadata = @{ kind = 'ui-automation'; depth = 'descendants'; all_desktops = $false }
|
|
385
|
+
}
|
|
241
386
|
'sysinfo' {
|
|
242
387
|
$out.system = @{
|
|
243
388
|
windows_version = [string](Get-CimInstance Win32_OperatingSystem).Caption
|
|
@@ -250,9 +395,8 @@ switch ($req.action) {
|
|
|
250
395
|
}
|
|
251
396
|
default { $out = @{ ok = $false; error = "unknown action: $($req.action)" } }
|
|
252
397
|
}
|
|
253
|
-
$out | ConvertTo-Json -Depth
|
|
398
|
+
$out | ConvertTo-Json -Depth 8 -Compress
|
|
254
399
|
`;
|
|
255
|
-
|
|
256
400
|
let psEnginePath = null;
|
|
257
401
|
function psEngine() {
|
|
258
402
|
if (psEnginePath && fs.existsSync(psEnginePath)) return psEnginePath;
|
|
@@ -384,54 +528,103 @@ const IMAGE_TOOLS = [
|
|
|
384
528
|
['spectacle', (p) => ['-b', '-n', '-o', p]],
|
|
385
529
|
];
|
|
386
530
|
|
|
531
|
+
const VK = {
|
|
532
|
+
ctrl: 0x11, control: 0x11, alt: 0x12, option: 0x12, shift: 0x10, win: 0x5b, super: 0x5b,
|
|
533
|
+
meta: 0x5b, cmd: 0x5b, command: 0x5b,
|
|
534
|
+
enter: 0x0d, return: 0x0d, tab: 0x09, esc: 0x1b, escape: 0x1b, space: 0x20, spacebar: 0x20,
|
|
535
|
+
backspace: 0x08, bs: 0x08, delete: 0x2e, del: 0x2e, insert: 0x2d, ins: 0x2d,
|
|
536
|
+
home: 0x24, end: 0x23, pageup: 0x21, pagedown: 0x22, pgup: 0x21, pgdn: 0x22,
|
|
537
|
+
up: 0x26, down: 0x28, left: 0x25, right: 0x27, arrowup: 0x26, arrowdown: 0x28,
|
|
538
|
+
arrowleft: 0x25, arrowright: 0x27,
|
|
539
|
+
capslock: 0x14, numlock: 0x90, scrolllock: 0x91, printscreen: 0x2c, prtsc: 0x2c,
|
|
540
|
+
pause: 0x13, menu: 0x5d, contextmenu: 0x5d, apps: 0x5d,
|
|
541
|
+
plus: 0xbb, minus: 0xbd, comma: 0xbc, period: 0xbe, slash: 0xbf, backslash: 0xdc,
|
|
542
|
+
semicolon: 0xba, quote: 0xde, bracketleft: 0xdb, bracketright: 0xdd, backtick: 0xc0,
|
|
543
|
+
numpad0: 0x60, numpad1: 0x61, numpad2: 0x62, numpad3: 0x63, numpad4: 0x64,
|
|
544
|
+
numpad5: 0x65, numpad6: 0x66, numpad7: 0x67, numpad8: 0x68, numpad9: 0x69,
|
|
545
|
+
};
|
|
546
|
+
for (let i = 1; i <= 24; i += 1) VK[`f${i}`] = 0x6f + i;
|
|
547
|
+
|
|
548
|
+
// keys that need KEYEVENTF_EXTENDEDKEY, like windows-use's kScExtendedVks
|
|
549
|
+
const EXTENDED = new Set([
|
|
550
|
+
0x2e, 0x2d, 0x24, 0x23, 0x21, 0x22, 0x26, 0x28, 0x25, 0x27, 0x5b, 0x5d, 0x2c, 0x90, 0x5d,
|
|
551
|
+
0x6f + 11, 0x6f + 12,
|
|
552
|
+
]);
|
|
553
|
+
const MODIFIERS = new Set(['ctrl', 'control', 'alt', 'option', 'shift', 'win', 'super', 'meta', 'cmd', 'command']);
|
|
554
|
+
|
|
387
555
|
function keyboardSequence(combo) {
|
|
388
|
-
// "ctrl+shift+s" -> [{vk, flags}]
|
|
389
|
-
// PowerShell engine sends through keybd_event. Unknown names are ignored.
|
|
390
|
-
const VK = {
|
|
391
|
-
ctrl: 0x11, control: 0x11, alt: 0x12, shift: 0x10, win: 0x5b, meta: 0x5b, cmd: 0x5b,
|
|
392
|
-
enter: 0x0d, return: 0x0d, tab: 0x09, esc: 0x1b, escape: 0x1b, space: 0x20,
|
|
393
|
-
backspace: 0x08, delete: 0x2e, del: 0x2e, insert: 0x2d, home: 0x24, end: 0x23,
|
|
394
|
-
pageup: 0x21, pagedown: 0x22, pgup: 0x21, pgdn: 0x22, up: 0x26, down: 0x28,
|
|
395
|
-
left: 0x25, right: 0x27, printscreen: 0x2c, capslock: 0x14, menu: 0x5d,
|
|
396
|
-
};
|
|
397
|
-
for (let i = 1; i <= 24; i += 1) VK[`f${i}`] = 0x6f + i;
|
|
556
|
+
// "ctrl+shift+s" -> [{ vk, flags, ext }] for keybd_event, the call windows-use makes too
|
|
398
557
|
const parts = String(combo || '')
|
|
399
558
|
.toLowerCase()
|
|
400
559
|
.split('+')
|
|
401
560
|
.map((p) => p.trim())
|
|
402
561
|
.filter(Boolean);
|
|
403
562
|
if (!parts.length) return [];
|
|
404
|
-
|
|
405
|
-
const repeat = parts.filter((p) => p === 'repeat').length;
|
|
406
|
-
const keys = parts.filter((p) => p !== 'repeat');
|
|
563
|
+
let repeat = 1;
|
|
407
564
|
const modVks = [];
|
|
408
565
|
const mainVks = [];
|
|
409
|
-
for (const key of
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
} else if (key.length === 1) {
|
|
414
|
-
mainVks.push(key.toUpperCase().charCodeAt(0));
|
|
566
|
+
for (const key of parts) {
|
|
567
|
+
if (key === 'repeat' || /^x\d+$/.test(key)) {
|
|
568
|
+
repeat = key === 'repeat' ? 2 : Number(key.slice(1));
|
|
569
|
+
continue;
|
|
415
570
|
}
|
|
571
|
+
let vk = VK[key];
|
|
572
|
+
if (vk === undefined && key.length === 1) vk = key.toUpperCase().charCodeAt(0);
|
|
573
|
+
if (vk === undefined) continue;
|
|
574
|
+
if (MODIFIERS.has(key)) modVks.push(vk);
|
|
575
|
+
else mainVks.push(vk);
|
|
416
576
|
}
|
|
417
|
-
const
|
|
577
|
+
const seq = [];
|
|
578
|
+
const times = Math.max(1, Math.min(50, repeat));
|
|
418
579
|
for (let i = 0; i < times; i += 1) {
|
|
419
580
|
if (!mainVks.length && modVks.length) {
|
|
420
581
|
// "win" on its own: press and release the modifier itself
|
|
421
582
|
const only = modVks[modVks.length - 1];
|
|
422
|
-
seq.push({ vk: only, flags: 0
|
|
583
|
+
seq.push({ vk: only, flags: 0, ext: EXTENDED.has(only) });
|
|
584
|
+
seq.push({ vk: only, flags: 2, ext: EXTENDED.has(only) });
|
|
423
585
|
continue;
|
|
424
586
|
}
|
|
425
|
-
for (const vk of modVks) seq.push({ vk, flags: 0 });
|
|
587
|
+
for (const vk of modVks) seq.push({ vk, flags: 0, ext: EXTENDED.has(vk) });
|
|
426
588
|
for (const vk of mainVks) {
|
|
427
|
-
seq.push({ vk, flags: 0 });
|
|
428
|
-
seq.push({ vk, flags: 2 });
|
|
589
|
+
seq.push({ vk, flags: 0, ext: EXTENDED.has(vk) });
|
|
590
|
+
seq.push({ vk, flags: 2, ext: EXTENDED.has(vk) });
|
|
429
591
|
}
|
|
430
|
-
for (const vk of [...modVks].reverse()) seq.push({ vk, flags: 2 });
|
|
592
|
+
for (const vk of [...modVks].reverse()) seq.push({ vk, flags: 2, ext: EXTENDED.has(vk) });
|
|
431
593
|
}
|
|
432
594
|
return seq;
|
|
433
595
|
}
|
|
434
596
|
|
|
597
|
+
// windows-use matches window names with fuzzy scores (wu::fuzzy::WRatio); the same
|
|
598
|
+
// idea keeps `ui app --name chrome` working when the title is "Google Chrome — …".
|
|
599
|
+
const fuzzyScore = (needle, haystack) => {
|
|
600
|
+
const a = String(needle || '').toLowerCase().trim();
|
|
601
|
+
const b = String(haystack || '').toLowerCase().trim();
|
|
602
|
+
if (!a || !b) return 0;
|
|
603
|
+
if (a === b) return 1;
|
|
604
|
+
if (b === a || b.startsWith(a)) return 0.95;
|
|
605
|
+
if (b.includes(a)) return 0.85;
|
|
606
|
+
const tokens = a.split(/\s+/).filter(Boolean);
|
|
607
|
+
const words = b.split(/\s+/).filter(Boolean);
|
|
608
|
+
const hits = tokens.filter((t) => words.some((w) => w.startsWith(t) || t.startsWith(w))).length;
|
|
609
|
+
if (hits) return 0.6 + (0.2 * hits) / tokens.length;
|
|
610
|
+
// character-level overlap, same spirit as partial_ratio
|
|
611
|
+
const chars = new Set(b.replace(/\s+/g, ''));
|
|
612
|
+
const overlap = [...a.replace(/\s+/g, '')].filter((c) => chars.has(c)).length;
|
|
613
|
+
return 0.4 * (overlap / Math.max(1, a.replace(/\s+/g, '').length));
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const AVOIDED = [/arenacode/i, /arena code/i];
|
|
617
|
+
|
|
618
|
+
function pickWindow(list, name) {
|
|
619
|
+
const candidates = (list || []).filter((w) => w && w.name && !AVOIDED.some((re) => re.test(w.name)));
|
|
620
|
+
let best = null;
|
|
621
|
+
for (const w of candidates) {
|
|
622
|
+
const score = fuzzyScore(name, w.name);
|
|
623
|
+
if (score >= 0.6 && (!best || score > best.score)) best = { ...w, score };
|
|
624
|
+
}
|
|
625
|
+
return best;
|
|
626
|
+
}
|
|
627
|
+
|
|
435
628
|
const automation = {
|
|
436
629
|
platform: process.platform,
|
|
437
630
|
|
|
@@ -471,16 +664,31 @@ const automation = {
|
|
|
471
664
|
|
|
472
665
|
windows() {
|
|
473
666
|
if (isWin) {
|
|
474
|
-
const r = winRun({ action: 'windows' });
|
|
475
|
-
|
|
667
|
+
const r = winRun({ action: 'windows' }, 45000);
|
|
668
|
+
if (!r.ok) return fail(r.error);
|
|
669
|
+
return { ok: true, windows: r.windows || [] };
|
|
476
670
|
}
|
|
477
671
|
const list = isMac ? macWindows() : linuxWindows();
|
|
478
|
-
return { ok: true, windows: list
|
|
672
|
+
return { ok: true, windows: list };
|
|
479
673
|
},
|
|
480
674
|
|
|
481
675
|
activeWindow() {
|
|
676
|
+
if (isWin) {
|
|
677
|
+
const r = winRun({ action: 'active' });
|
|
678
|
+
return r.ok ? { ok: true, name: r.name, handle: r.handle } : fail(r.error);
|
|
679
|
+
}
|
|
482
680
|
const w = this.windows();
|
|
483
|
-
|
|
681
|
+
const first = w.ok && w.windows.length ? w.windows[0] : null;
|
|
682
|
+
return { ok: true, name: first ? first.name : '', handle: first ? first.handle : null };
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
/** Finds the window the user means, the way windows-use does: best fuzzy match. */
|
|
686
|
+
findWindow(name) {
|
|
687
|
+
const list = this.windows();
|
|
688
|
+
if (!list.ok) return list;
|
|
689
|
+
const hit = pickWindow(list.windows, name);
|
|
690
|
+
if (!hit) return { ok: false, error: `window not found: ${name}` };
|
|
691
|
+
return { ok: true, window: hit };
|
|
484
692
|
},
|
|
485
693
|
|
|
486
694
|
launchApp(name) {
|
|
@@ -501,8 +709,10 @@ const automation = {
|
|
|
501
709
|
|
|
502
710
|
focusWindow(name) {
|
|
503
711
|
if (isWin) {
|
|
504
|
-
const
|
|
505
|
-
|
|
712
|
+
const found = this.findWindow(name);
|
|
713
|
+
if (!found.ok) return found;
|
|
714
|
+
const r = winRun({ action: 'focus', handle: found.window.handle });
|
|
715
|
+
return r.ok ? { ok: true, name: r.name, handle: found.window.handle, matched: found.window.name } : fail(r.error);
|
|
506
716
|
}
|
|
507
717
|
if (isMac) {
|
|
508
718
|
const res = osa(`tell application "System Events" to set frontmost of process ${JSON.stringify(name)} to true`);
|
|
@@ -514,7 +724,16 @@ const automation = {
|
|
|
514
724
|
|
|
515
725
|
resizeWindow(name, x, y, width, height) {
|
|
516
726
|
if (isWin) {
|
|
517
|
-
const
|
|
727
|
+
const found = this.findWindow(name);
|
|
728
|
+
if (!found.ok) return found;
|
|
729
|
+
const r = winRun({
|
|
730
|
+
action: 'resize',
|
|
731
|
+
handle: found.window.handle,
|
|
732
|
+
x,
|
|
733
|
+
y,
|
|
734
|
+
width,
|
|
735
|
+
height,
|
|
736
|
+
});
|
|
518
737
|
return r.ok ? { ok: true, name: r.name, x, y, width, height } : fail(r.error);
|
|
519
738
|
}
|
|
520
739
|
if (isMac) {
|
|
@@ -527,6 +746,52 @@ const automation = {
|
|
|
527
746
|
return res.ok ? { ok: true, name, x, y, width, height } : fail(res.stderr.trim() || 'resize failed');
|
|
528
747
|
},
|
|
529
748
|
|
|
749
|
+
/** The UI Automation tree of the focused window: what the desktop's windows-use returns. */
|
|
750
|
+
state(handle = null) {
|
|
751
|
+
if (isWin) {
|
|
752
|
+
const r = winRun({ action: 'state', handle }, 60000);
|
|
753
|
+
if (!r.ok) return fail(r.error);
|
|
754
|
+
return {
|
|
755
|
+
ok: true,
|
|
756
|
+
windows: r.windows || [],
|
|
757
|
+
active_window: r.active_window || null,
|
|
758
|
+
interactive_elements: r.interactive_elements || [],
|
|
759
|
+
scrollable_elements: r.scrollable_elements || [],
|
|
760
|
+
interactive_elements_text: r.interactive_elements_text || '',
|
|
761
|
+
scrollable_elements_text: r.scrollable_elements_text || '',
|
|
762
|
+
system: r.system || {},
|
|
763
|
+
metadata: r.metadata || {},
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// no accessibility tree on the other platforms: windows, geometry and pixels
|
|
767
|
+
const wins = this.windows();
|
|
768
|
+
const screen = this.screenSize();
|
|
769
|
+
const active = wins.ok && wins.windows.length ? wins.windows[0] : null;
|
|
770
|
+
const elements = (wins.ok ? wins.windows : []).map((w) => ({
|
|
771
|
+
name: w.name,
|
|
772
|
+
control_type: 'Window',
|
|
773
|
+
kind: 'Window',
|
|
774
|
+
bounding_box: { left: w.x, top: w.y, right: w.x + w.width, bottom: w.y + w.height },
|
|
775
|
+
center: { x: w.x + Math.floor(w.width / 2), y: w.y + Math.floor(w.height / 2) },
|
|
776
|
+
value: '',
|
|
777
|
+
enabled: true,
|
|
778
|
+
automation_id: '',
|
|
779
|
+
}));
|
|
780
|
+
return {
|
|
781
|
+
ok: true,
|
|
782
|
+
windows: wins.ok ? wins.windows : [],
|
|
783
|
+
active_window: active,
|
|
784
|
+
interactive_elements: elements,
|
|
785
|
+
scrollable_elements: [],
|
|
786
|
+
interactive_elements_text: elements
|
|
787
|
+
.map((e, i) => `${i + 1}. ${e.control_type} '${e.name}' at (${e.center.x}, ${e.center.y})`)
|
|
788
|
+
.join('\n'),
|
|
789
|
+
scrollable_elements_text: '',
|
|
790
|
+
system: this.sysinfo().system,
|
|
791
|
+
metadata: { kind: 'windows-only-list', depth: 'top-level', all_desktops: false },
|
|
792
|
+
};
|
|
793
|
+
},
|
|
794
|
+
|
|
530
795
|
click(x, y, { button = 'left', count = 1 } = {}) {
|
|
531
796
|
if (isWin) {
|
|
532
797
|
const r = winRun({ action: 'click', x, y, button, count });
|
|
@@ -644,11 +909,13 @@ const automation = {
|
|
|
644
909
|
return res.ok ? { ok: true, keys: combo } : fail(res.stderr.trim() || 'xdotool key failed');
|
|
645
910
|
},
|
|
646
911
|
|
|
647
|
-
screenshot(dest) {
|
|
912
|
+
screenshot(dest, { annotate = false } = {}) {
|
|
648
913
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
649
914
|
if (isWin) {
|
|
650
|
-
const r = winRun({ action: 'shot', path: dest },
|
|
651
|
-
return r.ok
|
|
915
|
+
const r = winRun({ action: 'shot', path: dest, annotate }, 90000);
|
|
916
|
+
return r.ok
|
|
917
|
+
? { ok: true, file: dest, width: r.width, height: r.height, mime: 'image/png', annotated: Boolean(annotate), elements: r.elements || 0 }
|
|
918
|
+
: fail(r.error);
|
|
652
919
|
}
|
|
653
920
|
if (isMac) {
|
|
654
921
|
const res = run('screencapture', ['-x', '-t', 'png', dest], { timeout: 30000 });
|
|
@@ -723,4 +990,75 @@ const automation = {
|
|
|
723
990
|
},
|
|
724
991
|
};
|
|
725
992
|
|
|
726
|
-
|
|
993
|
+
|
|
994
|
+
// --------------------------------------------------------- native FFI dispatch --
|
|
995
|
+
// Windows → windows-use.dll (shipped in the package, same DLL as the desktop app)
|
|
996
|
+
// Linux → Xlib + XTest through koffi, no external tools at all
|
|
997
|
+
// macOS → unchanged (osascript + cliclick)
|
|
998
|
+
// The backends expose the same shapes as the object above, so installing one only
|
|
999
|
+
// replaces the methods; the hand-written engines stay as a fallback.
|
|
1000
|
+
|
|
1001
|
+
let nativeBackend = null;
|
|
1002
|
+
let nativeKind = null;
|
|
1003
|
+
|
|
1004
|
+
try {
|
|
1005
|
+
if (isLinux) {
|
|
1006
|
+
nativeBackend = require('./automation/linux').backend;
|
|
1007
|
+
nativeKind = 'x11';
|
|
1008
|
+
} else if (isWin) {
|
|
1009
|
+
nativeBackend = require('./automation/win-dll').backend;
|
|
1010
|
+
nativeKind = 'dll';
|
|
1011
|
+
}
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
nativeBackend = null;
|
|
1014
|
+
nativeKind = null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (nativeBackend) {
|
|
1018
|
+
const handleFor = (hit) => (nativeKind === 'dll' ? hit.name : hit.handle);
|
|
1019
|
+
const resolve = (name) => {
|
|
1020
|
+
const list = nativeBackend.windows();
|
|
1021
|
+
if (!list.ok) return null;
|
|
1022
|
+
return pickWindow(list.windows, name);
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
Object.assign(automation, {
|
|
1026
|
+
native: { kind: nativeKind, name: nativeBackend.name, dll: nativeKind === 'dll' ? require('./automation/win-dll').locate() : null },
|
|
1027
|
+
available: () => nativeBackend.available(),
|
|
1028
|
+
screenSize: () => nativeBackend.screenSize(),
|
|
1029
|
+
windows: () => nativeBackend.windows(),
|
|
1030
|
+
activeWindow: () => nativeBackend.activeWindow(),
|
|
1031
|
+
findWindow(name) {
|
|
1032
|
+
const list = nativeBackend.windows();
|
|
1033
|
+
if (!list.ok) return list;
|
|
1034
|
+
const hit = pickWindow(list.windows, name);
|
|
1035
|
+
return hit ? { ok: true, window: hit } : fail(`window not found: ${name}`);
|
|
1036
|
+
},
|
|
1037
|
+
state: (handle = null) => nativeBackend.state(handle),
|
|
1038
|
+
moveMouse: (x, y) => nativeBackend.moveMouse(x, y),
|
|
1039
|
+
click: (x, y, opts = {}) => nativeBackend.click(x, y, opts),
|
|
1040
|
+
mouseButton: (x, y, button, down) => nativeBackend.mouseButton(x, y, button, down),
|
|
1041
|
+
drag: (x, y, x2, y2) => nativeBackend.drag(x, y, x2, y2),
|
|
1042
|
+
scroll: (amount, x = null, y = null) => nativeBackend.scroll(amount, x, y),
|
|
1043
|
+
typeText: (text, x = null, y = null) => nativeBackend.typeText(text, x, y),
|
|
1044
|
+
pressKeys: (combo) => nativeBackend.pressKeys(combo),
|
|
1045
|
+
screenshot: (dest, opts = {}) => nativeBackend.screenshot(dest, opts),
|
|
1046
|
+
listApps: () => nativeBackend.listApps(),
|
|
1047
|
+
sysinfo: () => nativeBackend.sysinfo(),
|
|
1048
|
+
launchApp: (name) => nativeBackend.launchApp(name),
|
|
1049
|
+
focusWindow(name) {
|
|
1050
|
+
const hit = resolve(name);
|
|
1051
|
+
if (!hit) return fail(`window not found: ${name}`);
|
|
1052
|
+
const answer = nativeBackend.focusWindow(handleFor(hit));
|
|
1053
|
+
return answer.ok ? { ...answer, name: hit.name, handle: hit.handle, matched: hit.name } : answer;
|
|
1054
|
+
},
|
|
1055
|
+
resizeWindow(name, x, y, width, height) {
|
|
1056
|
+
const hit = resolve(name);
|
|
1057
|
+
if (!hit) return fail(`window not found: ${name}`);
|
|
1058
|
+
const answer = nativeBackend.resizeWindow(handleFor(hit), x, y, width, height);
|
|
1059
|
+
return answer.ok ? { ...answer, name: hit.name, x, y, width, height } : answer;
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
module.exports = { ...automation, keyboardSequence, fuzzyScore, pickWindow };
|