ft-scout 8.0.2 → 8.0.3
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 +2 -2
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +105 -9
- 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 +72 -3
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/appControl.d.ts.map +1 -1
- package/bin/src/engine/appControl.js +522 -135
- package/bin/src/engine/appControl.js.map +1 -1
- package/bin/src/engine/scripts/winCapture.ps1 +248 -0
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/package.json +1 -1
- package/web/app.js +2 -2
- package/web/styles.css +1 -1
|
@@ -23,6 +23,9 @@ export function normalizeAppName(rawAppName) {
|
|
|
23
23
|
if (['code', 'vscode', 'visual-studio-code', 'cursor', 'zed', 'sublime', 'notepad', 'notepad++', 'vim', 'nvim', 'nano', 'editor'].includes(appLower)) {
|
|
24
24
|
return { category: 'editor', name: appLower || 'editor' };
|
|
25
25
|
}
|
|
26
|
+
if (['game', 'games', 'gaming', 'play', 'launcher', 'steam', 'epic', 'epicgames', 'riot', 'battlenet', 'ea', 'ubisoft', 'gog'].includes(appLower) || appLower.startsWith('game:')) {
|
|
27
|
+
return { category: 'game', name: appLower };
|
|
28
|
+
}
|
|
26
29
|
return { category: 'system', name: appLower };
|
|
27
30
|
}
|
|
28
31
|
export function parsePayload(payload) {
|
|
@@ -102,6 +105,78 @@ export function buildSocialUrl(platformOrAppName, target, text) {
|
|
|
102
105
|
}
|
|
103
106
|
return 'https://www.instagram.com/direct/inbox/';
|
|
104
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Universally search and locate any installed app, game, or store package on the host OS without hardcoding.
|
|
110
|
+
* Automatically resolves Win32 executables, Microsoft Store games, Steam games, and desktop shortcuts.
|
|
111
|
+
*/
|
|
112
|
+
export async function findInstalledApp(query) {
|
|
113
|
+
const cleanQuery = (query || '').replace(/[^\w\s-]/g, '').trim().toLowerCase();
|
|
114
|
+
if (!cleanQuery)
|
|
115
|
+
return null;
|
|
116
|
+
const platform = process.platform;
|
|
117
|
+
if (platform === 'win32') {
|
|
118
|
+
try {
|
|
119
|
+
// Query Windows Get-StartApps with wildcard matching
|
|
120
|
+
const safePattern = `*${cleanQuery.replace(/[*_']/g, '')}*`;
|
|
121
|
+
const psCmd = `powershell.exe -NoProfile -NonInteractive -Command "Get-StartApps -Name '${safePattern}' | Select-Object -First 1 Name, AppID | ConvertTo-Json"`;
|
|
122
|
+
const { stdout } = await execAsync(psCmd, { timeout: 3500 });
|
|
123
|
+
if (stdout && stdout.trim()) {
|
|
124
|
+
const parsed = JSON.parse(stdout.trim());
|
|
125
|
+
if (parsed && (parsed.Name || parsed.AppID)) {
|
|
126
|
+
return {
|
|
127
|
+
name: String(parsed.Name || query),
|
|
128
|
+
appId: String(parsed.AppID || ''),
|
|
129
|
+
source: 'start_apps',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
// Fallback: check registered URI scheme protocol (e.g. steam, roblox, spotify, discord, minecraft, riot)
|
|
136
|
+
if (!cleanQuery.includes(' ') && cleanQuery.length >= 3) {
|
|
137
|
+
try {
|
|
138
|
+
const checkReg = `powershell.exe -NoProfile -NonInteractive -Command "Test-Path 'Registry::HKEY_CLASSES_ROOT\\${cleanQuery}'"`;
|
|
139
|
+
const { stdout } = await execAsync(checkReg, { timeout: 1500 });
|
|
140
|
+
if (stdout && stdout.trim().toLowerCase() === 'true') {
|
|
141
|
+
return {
|
|
142
|
+
name: query,
|
|
143
|
+
appId: `${cleanQuery}:`,
|
|
144
|
+
source: 'protocol',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (platform === 'darwin') {
|
|
152
|
+
try {
|
|
153
|
+
const { stdout } = await execAsync(`mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == '*${cleanQuery}*'c" | head -n 1`, { timeout: 3000 });
|
|
154
|
+
if (stdout && stdout.trim()) {
|
|
155
|
+
return {
|
|
156
|
+
name: path.basename(stdout.trim(), '.app'),
|
|
157
|
+
appId: stdout.trim(),
|
|
158
|
+
source: 'path',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Linux: check which or desktop application entries
|
|
166
|
+
try {
|
|
167
|
+
const { stdout } = await execAsync(`which "${cleanQuery}" || find /usr/share/applications -name "*${cleanQuery}*.desktop" | head -n 1`, { timeout: 2500 });
|
|
168
|
+
if (stdout && stdout.trim()) {
|
|
169
|
+
return {
|
|
170
|
+
name: query,
|
|
171
|
+
appId: stdout.trim(),
|
|
172
|
+
source: 'path',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
105
180
|
/**
|
|
106
181
|
* Cross-platform app opener.
|
|
107
182
|
* Launches and opens target application on Windows, macOS, or Linux.
|
|
@@ -212,16 +287,42 @@ export async function openApp(appNameOrPath, target, options) {
|
|
|
212
287
|
}
|
|
213
288
|
}
|
|
214
289
|
else {
|
|
215
|
-
//
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
290
|
+
// Universal Application, Game & Desktop Launcher (Zero-Hardcode OS Discovery)
|
|
291
|
+
const targetQuery = name || appNameOrPath;
|
|
292
|
+
const installedApp = await findInstalledApp(targetQuery);
|
|
293
|
+
if (installedApp) {
|
|
294
|
+
if (platform === 'win32') {
|
|
295
|
+
if (installedApp.source === 'protocol') {
|
|
296
|
+
const extraArg = cleanTarget ? ` "${cleanTarget}"` : '';
|
|
297
|
+
command = `start ${installedApp.appId}${extraArg}`;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
command = `explorer.exe "shell:AppsFolder\\${installedApp.appId}"`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (platform === 'darwin') {
|
|
304
|
+
command = `open -a "${installedApp.appId || installedApp.name}"`;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
command = `"${installedApp.appId || installedApp.name}" &`;
|
|
308
|
+
}
|
|
222
309
|
}
|
|
223
310
|
else {
|
|
224
|
-
|
|
311
|
+
// Protocol scheme check (e.g. steam:, roblox:, spotify:, discord:, minecraft:, etc.)
|
|
312
|
+
const isProtocol = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) || (cleanTarget && /^[a-zA-Z0-9_\-]+:/.test(cleanTarget));
|
|
313
|
+
if (isProtocol && platform === 'win32') {
|
|
314
|
+
const proto = /^[a-zA-Z0-9_\-]+:/.test(appNameOrPath) ? appNameOrPath : cleanTarget;
|
|
315
|
+
command = `start ${proto}`;
|
|
316
|
+
}
|
|
317
|
+
else if (platform === 'win32') {
|
|
318
|
+
command = cleanTarget ? `start "" "${appNameOrPath}" ${cleanTarget}` : `start "" "${appNameOrPath}"`;
|
|
319
|
+
}
|
|
320
|
+
else if (platform === 'darwin') {
|
|
321
|
+
command = cleanTarget ? `open -a "${appNameOrPath}" "${cleanTarget}"` : `open -a "${appNameOrPath}"`;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
command = cleanTarget ? `${appNameOrPath} "${cleanTarget}" &` : `${appNameOrPath} &`;
|
|
325
|
+
}
|
|
225
326
|
}
|
|
226
327
|
}
|
|
227
328
|
try {
|
|
@@ -435,7 +536,19 @@ export async function executeInApp(appName, action, payload) {
|
|
|
435
536
|
if (category === 'browser') {
|
|
436
537
|
if (['navigate', 'goto', 'open_url'].includes(actLower)) {
|
|
437
538
|
const rawUrl = typeof payload === 'string' ? payload : (payload?.url || payload?.target || 'https://google.com');
|
|
438
|
-
|
|
539
|
+
let targetUrl = rawUrl.trim();
|
|
540
|
+
let navigationNote = '';
|
|
541
|
+
// Universal URL vs Search Query Grounding:
|
|
542
|
+
// If the input is not a valid URL (contains spaces or lacks a dot / scheme), convert into a search
|
|
543
|
+
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
|
544
|
+
if (targetUrl.includes(' ') || !targetUrl.includes('.')) {
|
|
545
|
+
navigationNote = ` [Note: Plain search term passed to navigate; converted to web search query]`;
|
|
546
|
+
targetUrl = `https://www.google.com/search?q=${encodeURIComponent(targetUrl)}`;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
targetUrl = `https://${targetUrl}`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
439
552
|
await openApp(name || 'browser', targetUrl);
|
|
440
553
|
await new Promise((r) => setTimeout(r, 800));
|
|
441
554
|
const capture = await captureScreen(name || 'browser');
|
|
@@ -443,7 +556,7 @@ export async function executeInApp(appName, action, payload) {
|
|
|
443
556
|
success: true,
|
|
444
557
|
app: name || 'browser',
|
|
445
558
|
action: actLower,
|
|
446
|
-
output: `Browser Navigated Successfully to Target URL: ${targetUrl}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
559
|
+
output: `Browser Navigated Successfully to Target URL: ${targetUrl}${navigationNote}\n- Screenshot Saved: ${capture.details?.outPath || 'Active'}`,
|
|
447
560
|
details: { url: targetUrl, capture: capture.details },
|
|
448
561
|
};
|
|
449
562
|
}
|
|
@@ -557,6 +670,10 @@ export async function executeInApp(appName, action, payload) {
|
|
|
557
670
|
return await openApp(name, filePath);
|
|
558
671
|
}
|
|
559
672
|
}
|
|
673
|
+
if (category === 'game' || ['open', 'launch', 'start'].includes(actLower)) {
|
|
674
|
+
const rawTarget = typeof payload === 'string' ? payload : (payload?.target || payload?.game || payload?.url || payload?.query || payload?.id || '');
|
|
675
|
+
return await openApp(name || appName, rawTarget);
|
|
676
|
+
}
|
|
560
677
|
// Generic fallback app action
|
|
561
678
|
return {
|
|
562
679
|
success: true,
|
|
@@ -617,49 +734,291 @@ export function getAppCapabilities() {
|
|
|
617
734
|
defaultEditorLauncher: platform === 'win32' ? 'code / notepad' : platform === 'darwin' ? 'code / open -a' : 'code / xdg-open',
|
|
618
735
|
};
|
|
619
736
|
}
|
|
737
|
+
const WIN_CAPTURE_SCRIPT_CONTENT = `param(
|
|
738
|
+
[string]$OutPath,
|
|
739
|
+
[switch]$InfoOnly
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
$source = @"
|
|
743
|
+
using System;
|
|
744
|
+
using System.Drawing;
|
|
745
|
+
using System.Drawing.Imaging;
|
|
746
|
+
using System.Runtime.InteropServices;
|
|
747
|
+
using System.Text;
|
|
748
|
+
using System.Threading;
|
|
749
|
+
using System.Collections.Generic;
|
|
750
|
+
|
|
751
|
+
public class WinCaptureResult {
|
|
752
|
+
public bool success;
|
|
753
|
+
public string savedPath;
|
|
754
|
+
public int width;
|
|
755
|
+
public int height;
|
|
756
|
+
public int cursorX;
|
|
757
|
+
public int cursorY;
|
|
758
|
+
public string activeWindow;
|
|
759
|
+
public string[] visibleWindows;
|
|
760
|
+
public string error;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
public class WinCaptureHelper {
|
|
764
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
765
|
+
public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
766
|
+
|
|
767
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
768
|
+
public static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
|
|
769
|
+
|
|
770
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
771
|
+
public static extern bool SetThreadDesktop(IntPtr hDesktop);
|
|
772
|
+
|
|
773
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
774
|
+
public static extern bool CloseDesktop(IntPtr hDesktop);
|
|
775
|
+
|
|
776
|
+
[DllImport("user32.dll")]
|
|
777
|
+
public static extern IntPtr GetDC(IntPtr hWnd);
|
|
778
|
+
|
|
779
|
+
[DllImport("user32.dll")]
|
|
780
|
+
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
|
781
|
+
|
|
782
|
+
[DllImport("gdi32.dll")]
|
|
783
|
+
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
|
784
|
+
|
|
785
|
+
[DllImport("gdi32.dll")]
|
|
786
|
+
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
|
|
787
|
+
|
|
788
|
+
[DllImport("gdi32.dll")]
|
|
789
|
+
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
|
790
|
+
|
|
791
|
+
[DllImport("gdi32.dll")]
|
|
792
|
+
public static extern bool DeleteDC(IntPtr hdc);
|
|
793
|
+
|
|
794
|
+
[DllImport("gdi32.dll")]
|
|
795
|
+
public static extern bool DeleteObject(IntPtr hObject);
|
|
796
|
+
|
|
797
|
+
[DllImport("gdi32.dll")]
|
|
798
|
+
public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
|
|
799
|
+
|
|
800
|
+
[DllImport("user32.dll")]
|
|
801
|
+
public static extern int GetSystemMetrics(int nIndex);
|
|
802
|
+
|
|
803
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
804
|
+
|
|
805
|
+
[DllImport("user32.dll")]
|
|
806
|
+
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
807
|
+
|
|
808
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
|
809
|
+
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
810
|
+
|
|
811
|
+
[DllImport("user32.dll")]
|
|
812
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
813
|
+
|
|
814
|
+
[DllImport("user32.dll")]
|
|
815
|
+
public static extern IntPtr GetForegroundWindow();
|
|
816
|
+
|
|
817
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
818
|
+
public struct POINT {
|
|
819
|
+
public int X;
|
|
820
|
+
public int Y;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
[DllImport("user32.dll")]
|
|
824
|
+
public static extern bool GetCursorPos(out POINT lpPoint);
|
|
825
|
+
|
|
826
|
+
const uint DESKTOP_ACCESS = 0x01FF | 0x00020000;
|
|
827
|
+
const int SM_CXSCREEN = 0;
|
|
828
|
+
const int SM_CYSCREEN = 1;
|
|
829
|
+
const int SRCCOPY = 0x00CC0020;
|
|
830
|
+
const int CAPTUREBLT = 0x40000000;
|
|
831
|
+
|
|
832
|
+
public static WinCaptureResult GetInfo() {
|
|
833
|
+
var res = new WinCaptureResult();
|
|
834
|
+
res.success = true;
|
|
835
|
+
res.error = "";
|
|
836
|
+
var visibleWindows = new List<string>();
|
|
837
|
+
|
|
838
|
+
Thread worker = new Thread(() => {
|
|
839
|
+
IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
|
|
840
|
+
if (hDesk == IntPtr.Zero) {
|
|
841
|
+
hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
|
|
842
|
+
}
|
|
843
|
+
if (hDesk != IntPtr.Zero) {
|
|
844
|
+
SetThreadDesktop(hDesk);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
res.width = GetSystemMetrics(SM_CXSCREEN);
|
|
848
|
+
res.height = GetSystemMetrics(SM_CYSCREEN);
|
|
849
|
+
if (res.width <= 0) res.width = 1920;
|
|
850
|
+
if (res.height <= 0) res.height = 1080;
|
|
851
|
+
|
|
852
|
+
POINT pt;
|
|
853
|
+
if (GetCursorPos(out pt)) {
|
|
854
|
+
res.cursorX = pt.X;
|
|
855
|
+
res.cursorY = pt.Y;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
IntPtr fg = GetForegroundWindow();
|
|
859
|
+
if (fg != IntPtr.Zero) {
|
|
860
|
+
var sbFg = new StringBuilder(256);
|
|
861
|
+
GetWindowText(fg, sbFg, 256);
|
|
862
|
+
res.activeWindow = sbFg.ToString().Trim();
|
|
863
|
+
} else {
|
|
864
|
+
res.activeWindow = "";
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
EnumWindows((hWnd, lParam) => {
|
|
868
|
+
if (IsWindowVisible(hWnd)) {
|
|
869
|
+
var title = new StringBuilder(256);
|
|
870
|
+
GetWindowText(hWnd, title, 256);
|
|
871
|
+
string t = title.ToString().Trim();
|
|
872
|
+
if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
|
|
873
|
+
visibleWindows.Add(t);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
}, IntPtr.Zero);
|
|
878
|
+
|
|
879
|
+
if (hDesk != IntPtr.Zero) {
|
|
880
|
+
CloseDesktop(hDesk);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
worker.SetApartmentState(ApartmentState.STA);
|
|
885
|
+
worker.Start();
|
|
886
|
+
worker.Join();
|
|
887
|
+
|
|
888
|
+
res.visibleWindows = visibleWindows.ToArray();
|
|
889
|
+
return res;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
public static WinCaptureResult Capture(string filePath) {
|
|
893
|
+
var res = new WinCaptureResult();
|
|
894
|
+
res.savedPath = filePath;
|
|
895
|
+
res.error = "";
|
|
896
|
+
var visibleWindows = new List<string>();
|
|
897
|
+
|
|
898
|
+
Thread worker = new Thread(() => {
|
|
899
|
+
IntPtr hDesk = OpenInputDesktop(0, false, DESKTOP_ACCESS);
|
|
900
|
+
if (hDesk == IntPtr.Zero) {
|
|
901
|
+
hDesk = OpenDesktop("Default", 0, false, DESKTOP_ACCESS);
|
|
902
|
+
}
|
|
903
|
+
if (hDesk != IntPtr.Zero) {
|
|
904
|
+
SetThreadDesktop(hDesk);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
res.width = GetSystemMetrics(SM_CXSCREEN);
|
|
908
|
+
res.height = GetSystemMetrics(SM_CYSCREEN);
|
|
909
|
+
if (res.width <= 0) res.width = 1920;
|
|
910
|
+
if (res.height <= 0) res.height = 1080;
|
|
911
|
+
|
|
912
|
+
POINT pt;
|
|
913
|
+
if (GetCursorPos(out pt)) {
|
|
914
|
+
res.cursorX = pt.X;
|
|
915
|
+
res.cursorY = pt.Y;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
IntPtr fg = GetForegroundWindow();
|
|
919
|
+
if (fg != IntPtr.Zero) {
|
|
920
|
+
var sbFg = new StringBuilder(256);
|
|
921
|
+
GetWindowText(fg, sbFg, 256);
|
|
922
|
+
res.activeWindow = sbFg.ToString().Trim();
|
|
923
|
+
} else {
|
|
924
|
+
res.activeWindow = "";
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
EnumWindows((hWnd, lParam) => {
|
|
928
|
+
if (IsWindowVisible(hWnd)) {
|
|
929
|
+
var title = new StringBuilder(256);
|
|
930
|
+
GetWindowText(hWnd, title, 256);
|
|
931
|
+
string t = title.ToString().Trim();
|
|
932
|
+
if (t.Length > 0 && !visibleWindows.Contains(t) && t != "Program Manager" && t != "Windows Input Experience") {
|
|
933
|
+
visibleWindows.Add(t);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return true;
|
|
937
|
+
}, IntPtr.Zero);
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
IntPtr hdcSrc = GetDC(IntPtr.Zero);
|
|
941
|
+
IntPtr hdcDest = CreateCompatibleDC(hdcSrc);
|
|
942
|
+
IntPtr hBitmap = CreateCompatibleBitmap(hdcSrc, res.width, res.height);
|
|
943
|
+
IntPtr hOld = SelectObject(hdcDest, hBitmap);
|
|
944
|
+
bool bltOk = BitBlt(hdcDest, 0, 0, res.width, res.height, hdcSrc, 0, 0, SRCCOPY | CAPTUREBLT);
|
|
945
|
+
|
|
946
|
+
SelectObject(hdcDest, hOld);
|
|
947
|
+
DeleteDC(hdcDest);
|
|
948
|
+
ReleaseDC(IntPtr.Zero, hdcSrc);
|
|
949
|
+
|
|
950
|
+
if (bltOk) {
|
|
951
|
+
using (Bitmap bmp = Bitmap.FromHbitmap(hBitmap)) {
|
|
952
|
+
bmp.Save(filePath, ImageFormat.Png);
|
|
953
|
+
}
|
|
954
|
+
res.success = true;
|
|
955
|
+
} else {
|
|
956
|
+
res.error = "BitBlt failed";
|
|
957
|
+
}
|
|
958
|
+
DeleteObject(hBitmap);
|
|
959
|
+
} catch (Exception ex) {
|
|
960
|
+
res.error = ex.Message;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (hDesk != IntPtr.Zero) {
|
|
964
|
+
CloseDesktop(hDesk);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
worker.SetApartmentState(ApartmentState.STA);
|
|
969
|
+
worker.Start();
|
|
970
|
+
worker.Join();
|
|
971
|
+
|
|
972
|
+
res.visibleWindows = visibleWindows.ToArray();
|
|
973
|
+
return res;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
"@
|
|
977
|
+
|
|
978
|
+
Add-Type -TypeDefinition $source -ReferencedAssemblies System.Drawing, System.Windows.Forms
|
|
979
|
+
|
|
980
|
+
if ($InfoOnly) {
|
|
981
|
+
[WinCaptureHelper]::GetInfo() | ConvertTo-Json -Compress
|
|
982
|
+
} else {
|
|
983
|
+
[WinCaptureHelper]::Capture($OutPath) | ConvertTo-Json -Compress
|
|
984
|
+
}
|
|
985
|
+
`;
|
|
986
|
+
export function ensureWinCaptureScript() {
|
|
987
|
+
const scriptsDir = path.join(process.cwd(), '.ft', 'scripts');
|
|
988
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
989
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
990
|
+
}
|
|
991
|
+
const scriptPath = path.join(scriptsDir, 'winCapture.ps1');
|
|
992
|
+
fs.writeFileSync(scriptPath, WIN_CAPTURE_SCRIPT_CONTENT, 'utf8');
|
|
993
|
+
return scriptPath;
|
|
994
|
+
}
|
|
620
995
|
/**
|
|
621
|
-
* Get screen metrics, active window title, and cursor position.
|
|
996
|
+
* Get screen metrics, active window title, visible windows, and cursor position.
|
|
622
997
|
*/
|
|
623
998
|
export async function getScreenInfo(appName) {
|
|
624
999
|
const platform = process.platform;
|
|
625
1000
|
if (platform === 'win32') {
|
|
626
|
-
const psScript = `
|
|
627
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
628
|
-
Add-Type -AssemblyName System.Drawing
|
|
629
|
-
|
|
630
|
-
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
631
|
-
$cursor = [System.Windows.Forms.Cursor]::Position
|
|
632
|
-
|
|
633
|
-
$sig = '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);'
|
|
634
|
-
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
635
|
-
$hwnd = $w::GetForegroundWindow()
|
|
636
|
-
$sb = New-Object System.Text.StringBuilder 256
|
|
637
|
-
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
638
|
-
$activeTitle = $sb.ToString()
|
|
639
|
-
|
|
640
|
-
@{
|
|
641
|
-
screenWidth = $screen.Bounds.Width
|
|
642
|
-
screenHeight = $screen.Bounds.Height
|
|
643
|
-
cursorX = $cursor.X
|
|
644
|
-
cursorY = $cursor.Y
|
|
645
|
-
activeWindow = $activeTitle
|
|
646
|
-
} | ConvertTo-Json
|
|
647
|
-
`;
|
|
648
1001
|
try {
|
|
649
|
-
const
|
|
650
|
-
const { stdout } = await execAsync(`powershell -NoProfile -
|
|
1002
|
+
const scriptPath = ensureWinCaptureScript();
|
|
1003
|
+
const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -InfoOnly`);
|
|
651
1004
|
const data = JSON.parse(stdout.trim());
|
|
1005
|
+
const visList = Array.isArray(data.visibleWindows) && data.visibleWindows.length > 0
|
|
1006
|
+
? ` | Visible Windows: ${data.visibleWindows.slice(0, 5).join(', ')}`
|
|
1007
|
+
: '';
|
|
652
1008
|
return {
|
|
653
1009
|
success: true,
|
|
654
|
-
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || 'Desktop'}"`,
|
|
655
|
-
details:
|
|
1010
|
+
output: `Screen Info: Resolution ${data.screenWidth}x${data.screenHeight} | Cursor at (${data.cursorX}, ${data.cursorY}) | Active Window: "${data.activeWindow || appName || 'Desktop'}"${visList}`,
|
|
1011
|
+
details: {
|
|
1012
|
+
...data,
|
|
1013
|
+
activeWindow: data.activeWindow || appName || 'Desktop',
|
|
1014
|
+
},
|
|
656
1015
|
};
|
|
657
1016
|
}
|
|
658
|
-
catch
|
|
1017
|
+
catch {
|
|
659
1018
|
return {
|
|
660
1019
|
success: true,
|
|
661
1020
|
output: `Screen Info (Default): Resolution 1920x1080 | Active Window: "${appName || 'Desktop'}"`,
|
|
662
|
-
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop' },
|
|
1021
|
+
details: { screenWidth: 1920, screenHeight: 1080, activeWindow: appName || 'Desktop', cursorX: 0, cursorY: 0 },
|
|
663
1022
|
};
|
|
664
1023
|
}
|
|
665
1024
|
}
|
|
@@ -897,51 +1256,56 @@ export async function captureScreen(appNameOrScreen) {
|
|
|
897
1256
|
const filename = `screen_${Date.now()}.png`;
|
|
898
1257
|
const outPath = path.join(screenshotsDir, filename);
|
|
899
1258
|
if (platform === 'win32') {
|
|
900
|
-
|
|
1259
|
+
try {
|
|
1260
|
+
const scriptPath = ensureWinCaptureScript();
|
|
1261
|
+
const { stdout } = await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -OutPath "${outPath}"`);
|
|
1262
|
+
const meta = JSON.parse(stdout.trim());
|
|
1263
|
+
if (meta.success && fs.existsSync(outPath) && fs.statSync(outPath).size > 0) {
|
|
1264
|
+
registerCapturedScreenshot(outPath);
|
|
1265
|
+
showScreenAuraOverlay(1800, "Scout is viewing the screen...", false).catch(() => { });
|
|
1266
|
+
const visibleList = Array.isArray(meta.visibleWindows) && meta.visibleWindows.length > 0
|
|
1267
|
+
? `\n- Visible Windows: ${meta.visibleWindows.slice(0, 6).map((w) => `"${w}"`).join(', ')}`
|
|
1268
|
+
: '';
|
|
1269
|
+
return {
|
|
1270
|
+
success: true,
|
|
1271
|
+
output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"${visibleList}\n- Seeing Status: Screen is visible and captured. (NOTE: This temporary screenshot will be automatically deleted upon task completion).`,
|
|
1272
|
+
details: { ...meta, outPath },
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
throw new Error(meta.error || 'Desktop capture returned false');
|
|
1276
|
+
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
// Fallback to Graphics.CopyFromScreen
|
|
1279
|
+
const psScript = `
|
|
901
1280
|
Add-Type -AssemblyName System.Windows.Forms
|
|
902
1281
|
Add-Type -AssemblyName System.Drawing
|
|
903
|
-
|
|
904
1282
|
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
905
1283
|
$bounds = $screen.Bounds
|
|
906
1284
|
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
907
1285
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
|
908
|
-
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
1286
|
+
try { $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) } catch {}
|
|
909
1287
|
$bmp.Save("${outPath.replace(/\\/g, '\\\\')}", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
910
1288
|
$graphics.Dispose()
|
|
911
1289
|
$bmp.Dispose()
|
|
912
|
-
|
|
913
|
-
$sig = '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);'
|
|
914
|
-
$w = Add-Type -memberDefinition $sig -name 'Win32Window' -namespace Win32Functions -passThru
|
|
915
|
-
$hwnd = $w::GetForegroundWindow()
|
|
916
|
-
$sb = New-Object System.Text.StringBuilder 256
|
|
917
|
-
[void]$w::GetWindowText($hwnd, $sb, 256)
|
|
918
|
-
$activeTitle = $sb.ToString()
|
|
919
|
-
|
|
920
|
-
@{
|
|
921
|
-
savedPath = "${outPath.replace(/\\/g, '\\\\')}"
|
|
922
|
-
width = $bounds.Width
|
|
923
|
-
height = $bounds.Height
|
|
924
|
-
activeWindow = $activeTitle
|
|
925
|
-
} | ConvertTo-Json
|
|
1290
|
+
@{ savedPath = "${outPath.replace(/\\/g, '\\\\')}"; width = $bounds.Width; height = $bounds.Height; activeWindow = "${appNameOrScreen || 'Desktop'}" } | ConvertTo-Json
|
|
926
1291
|
`;
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
};
|
|
1292
|
+
try {
|
|
1293
|
+
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
1294
|
+
const { stdout } = await execAsync(`powershell -NoProfile -EncodedCommand ${encodedScript}`);
|
|
1295
|
+
const meta = JSON.parse(stdout.trim());
|
|
1296
|
+
registerCapturedScreenshot(outPath);
|
|
1297
|
+
return {
|
|
1298
|
+
success: true,
|
|
1299
|
+
output: `Screen Captured Successfully!\n- Screenshot Saved To: ${outPath}\n- Resolution: ${meta.width}x${meta.height}\n- Active Window: "${meta.activeWindow || 'Desktop'}"\n- Seeing Status: Screen is visible and captured.`,
|
|
1300
|
+
details: { ...meta, outPath },
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
catch (fallbackErr) {
|
|
1304
|
+
return {
|
|
1305
|
+
success: false,
|
|
1306
|
+
output: `Failed to capture screen: ${err?.message || fallbackErr?.message || String(err)}`,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
945
1309
|
}
|
|
946
1310
|
}
|
|
947
1311
|
else if (platform === 'darwin') {
|
|
@@ -979,6 +1343,64 @@ $activeTitle = $sb.ToString()
|
|
|
979
1343
|
}
|
|
980
1344
|
}
|
|
981
1345
|
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Call Vision AI with graceful multi-provider fallback.
|
|
1348
|
+
* Tries NVIDIA Vision (meta/llama-3.2-11b-vision-instruct), Gemini (gemini-2.0-flash), OpenAI (gpt-4o-mini), and BYOK.
|
|
1349
|
+
*/
|
|
1350
|
+
export async function callVisionAi(outPath, prompt, maxTokens = 350) {
|
|
1351
|
+
if (!outPath || !fs.existsSync(outPath))
|
|
1352
|
+
return null;
|
|
1353
|
+
let imageBase64;
|
|
1354
|
+
try {
|
|
1355
|
+
imageBase64 = fs.readFileSync(outPath).toString('base64');
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
const nvidiaKey = process.env.NVIDIA_API_KEY || process.env.NVCF_API_KEY;
|
|
1361
|
+
const geminiKey = process.env.GEMINI_API_KEY;
|
|
1362
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1363
|
+
const byokKey = process.env.BYOK_API_KEY;
|
|
1364
|
+
const candidates = [];
|
|
1365
|
+
if (nvidiaKey) {
|
|
1366
|
+
candidates.push({ apiKey: nvidiaKey, baseURL: 'https://integrate.api.nvidia.com/v1', model: 'meta/llama-3.2-11b-vision-instruct', timeout: 35000 });
|
|
1367
|
+
}
|
|
1368
|
+
if (geminiKey) {
|
|
1369
|
+
candidates.push({ apiKey: geminiKey, baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.0-flash', timeout: 20000 });
|
|
1370
|
+
}
|
|
1371
|
+
if (openaiKey) {
|
|
1372
|
+
candidates.push({ apiKey: openaiKey, model: 'gpt-4o-mini', timeout: 20000 });
|
|
1373
|
+
}
|
|
1374
|
+
if (byokKey) {
|
|
1375
|
+
candidates.push({ apiKey: byokKey, model: 'gpt-4o-mini', timeout: 20000 });
|
|
1376
|
+
}
|
|
1377
|
+
for (const cand of candidates) {
|
|
1378
|
+
try {
|
|
1379
|
+
const client = new OpenAI({ apiKey: cand.apiKey, baseURL: cand.baseURL, timeout: cand.timeout });
|
|
1380
|
+
const resp = await client.chat.completions.create({
|
|
1381
|
+
model: cand.model,
|
|
1382
|
+
messages: [
|
|
1383
|
+
{
|
|
1384
|
+
role: 'user',
|
|
1385
|
+
content: [
|
|
1386
|
+
{ type: 'text', text: prompt },
|
|
1387
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
|
1388
|
+
],
|
|
1389
|
+
},
|
|
1390
|
+
],
|
|
1391
|
+
max_tokens: maxTokens,
|
|
1392
|
+
});
|
|
1393
|
+
const reply = resp.choices[0]?.message?.content?.trim();
|
|
1394
|
+
if (reply) {
|
|
1395
|
+
return reply;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
catch {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
982
1404
|
/**
|
|
983
1405
|
* Visual screen analysis. Takes a full-screen screenshot and provides visual context
|
|
984
1406
|
* (including AI Vision inspection if an API key is available).
|
|
@@ -989,36 +1411,14 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
|
|
|
989
1411
|
const info = await getScreenInfo(appNameOrScreen);
|
|
990
1412
|
let visualAiAnalysis = '';
|
|
991
1413
|
const outPath = capture.details?.outPath;
|
|
992
|
-
// Optional Vision Model Analysis if screenshot exists and
|
|
1414
|
+
// Optional Vision Model Analysis if screenshot exists and has valid content
|
|
993
1415
|
if (capture.success && outPath && fs.existsSync(outPath)) {
|
|
994
1416
|
try {
|
|
995
|
-
|
|
996
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
997
|
-
const byokKey = process.env.BYOK_API_KEY;
|
|
998
|
-
const apiKey = geminiKey || openaiKey || byokKey;
|
|
999
|
-
if (apiKey) {
|
|
1000
|
-
const imageBase64 = fs.readFileSync(outPath).toString('base64');
|
|
1001
|
-
const isGemini = Boolean(geminiKey);
|
|
1002
|
-
const baseURL = isGemini ? 'https://generativelanguage.googleapis.com/v1beta/openai' : undefined;
|
|
1003
|
-
const model = isGemini ? 'gemini-2.5-flash' : 'gpt-4o-mini';
|
|
1004
|
-
const client = new OpenAI({ apiKey, baseURL, timeout: 15000 });
|
|
1417
|
+
if (fs.statSync(outPath).size > 0) {
|
|
1005
1418
|
const prompt = targetQuery
|
|
1006
1419
|
? `Describe what is visible on this screen specifically regarding: "${targetQuery}". Keep it concise, noting visible UI elements, open windows, tabs, buttons, or error messages.`
|
|
1007
|
-
: `Briefly describe what is currently visible on this desktop screen: active window, main content/tabs, buttons, and system state.`;
|
|
1008
|
-
const
|
|
1009
|
-
model,
|
|
1010
|
-
messages: [
|
|
1011
|
-
{
|
|
1012
|
-
role: 'user',
|
|
1013
|
-
content: [
|
|
1014
|
-
{ type: 'text', text: prompt },
|
|
1015
|
-
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
|
1016
|
-
],
|
|
1017
|
-
},
|
|
1018
|
-
],
|
|
1019
|
-
max_tokens: 350,
|
|
1020
|
-
});
|
|
1021
|
-
const reply = resp.choices[0]?.message?.content;
|
|
1420
|
+
: `Briefly describe what is currently visible on this desktop screen: active window, main content/tabs, buttons, and system state in 2-3 sentences.`;
|
|
1421
|
+
const reply = await callVisionAi(outPath, prompt, 350);
|
|
1022
1422
|
if (reply && reply.trim()) {
|
|
1023
1423
|
visualAiAnalysis = `\nAI Visual Screen Analysis:\n${reply.trim()}`;
|
|
1024
1424
|
}
|
|
@@ -1028,12 +1428,19 @@ export async function analyzeScreen(appNameOrScreen, targetQuery) {
|
|
|
1028
1428
|
// Vision API call is optional; fallback gracefully to system metadata
|
|
1029
1429
|
}
|
|
1030
1430
|
}
|
|
1431
|
+
const activeWin = capture.details?.activeWindow || info.details?.activeWindow || appNameOrScreen || 'Active Desktop';
|
|
1432
|
+
const visWins = Array.isArray(capture.details?.visibleWindows) && capture.details.visibleWindows.length > 0
|
|
1433
|
+
? capture.details.visibleWindows
|
|
1434
|
+
: (Array.isArray(info.details?.visibleWindows) ? info.details.visibleWindows : []);
|
|
1435
|
+
const visibleWindowsText = visWins.length > 0
|
|
1436
|
+
? `\nVisible Open Windows:\n${visWins.slice(0, 8).map((w) => ` - "${w}"`).join('\n')}`
|
|
1437
|
+
: '';
|
|
1031
1438
|
const report = [
|
|
1032
1439
|
`=== SCOUT SCRATCHPAD DESKTOP SCREEN VISUAL INSPECTION ===`,
|
|
1033
1440
|
`Target App / Screen Context: "${appNameOrScreen || 'Desktop'}"`,
|
|
1034
1441
|
`Screen Resolution: ${info.details?.screenWidth || capture.details?.width || 1920}x${info.details?.screenHeight || capture.details?.height || 1080}`,
|
|
1035
|
-
`Active Window Focus: "${
|
|
1036
|
-
`Mouse Cursor Position: (${info.details?.cursorX || 0}, ${info.details?.cursorY || 0})`,
|
|
1442
|
+
`Active Window Focus: "${activeWin}"${visibleWindowsText}`,
|
|
1443
|
+
`Mouse Cursor Position: (${info.details?.cursorX || capture.details?.cursorX || 0}, ${info.details?.cursorY || capture.details?.cursorY || 0})`,
|
|
1037
1444
|
`Screenshot Status: Captured temporarily for visual inspection. (Will be automatically deleted upon task completion)`,
|
|
1038
1445
|
`Visual Status: Screen is visible and verified.${visualAiAnalysis}`,
|
|
1039
1446
|
].join('\n');
|
|
@@ -1548,45 +1955,25 @@ export async function findAndClickElement(elementQuery, appName, button = 'left'
|
|
|
1548
1955
|
const outPath = capture.details.outPath;
|
|
1549
1956
|
let targetX = undefined;
|
|
1550
1957
|
let targetY = undefined;
|
|
1551
|
-
|
|
1552
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1553
|
-
const byokKey = process.env.BYOK_API_KEY;
|
|
1554
|
-
const apiKey = geminiKey || openaiKey || byokKey;
|
|
1555
|
-
if (apiKey && fs.existsSync(outPath)) {
|
|
1958
|
+
if (fs.existsSync(outPath)) {
|
|
1556
1959
|
try {
|
|
1557
|
-
const imageBase64 = fs.readFileSync(outPath).toString('base64');
|
|
1558
|
-
const isGemini = Boolean(geminiKey);
|
|
1559
|
-
const baseURL = isGemini ? 'https://generativelanguage.googleapis.com/v1beta/openai' : undefined;
|
|
1560
|
-
const model = isGemini ? 'gemini-2.5-flash' : 'gpt-4o-mini';
|
|
1561
|
-
const client = new OpenAI({ apiKey, baseURL, timeout: 15000 });
|
|
1562
1960
|
const prompt = `Look at this screenshot and locate the UI element: "${elementQuery}".
|
|
1563
1961
|
Return ONLY a valid JSON object with the center point of this element in 0-1000 normalized coordinates:
|
|
1564
1962
|
{"point": [y, x]}
|
|
1565
1963
|
Where y is between 0 and 1000 (top to bottom) and x is between 0 and 1000 (left to right).
|
|
1566
1964
|
If not found, return {"point": null}. Output nothing else.`;
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
});
|
|
1580
|
-
const reply = resp.choices[0]?.message?.content || '';
|
|
1581
|
-
const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
|
|
1582
|
-
if (match) {
|
|
1583
|
-
const parsed = JSON.parse(match[0]);
|
|
1584
|
-
if (Array.isArray(parsed.point) && parsed.point.length === 2) {
|
|
1585
|
-
const normY = Number(parsed.point[0]);
|
|
1586
|
-
const normX = Number(parsed.point[1]);
|
|
1587
|
-
if (!isNaN(normX) && !isNaN(normY)) {
|
|
1588
|
-
targetX = Math.round((normX / 1000) * screenWidth);
|
|
1589
|
-
targetY = Math.round((normY / 1000) * screenHeight);
|
|
1965
|
+
const reply = await callVisionAi(outPath, prompt, 80);
|
|
1966
|
+
if (reply) {
|
|
1967
|
+
const match = reply.match(/\{[\s\S]*"point"[\s\S]*\}/);
|
|
1968
|
+
if (match) {
|
|
1969
|
+
const parsed = JSON.parse(match[0]);
|
|
1970
|
+
if (Array.isArray(parsed.point) && parsed.point.length === 2) {
|
|
1971
|
+
const normY = Number(parsed.point[0]);
|
|
1972
|
+
const normX = Number(parsed.point[1]);
|
|
1973
|
+
if (!isNaN(normX) && !isNaN(normY)) {
|
|
1974
|
+
targetX = Math.round((normX / 1000) * screenWidth);
|
|
1975
|
+
targetY = Math.round((normY / 1000) * screenHeight);
|
|
1976
|
+
}
|
|
1590
1977
|
}
|
|
1591
1978
|
}
|
|
1592
1979
|
}
|