@leg3ndy/otto-bridge 0.5.5 → 0.5.6
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/dist/executors/native_macos.js +389 -27
- package/dist/types.js +1 -1
- package/package.json +1 -1
|
@@ -132,7 +132,132 @@ function extractMeaningfulDescriptionTokens(value) {
|
|
|
132
132
|
function descriptionLikelyHasTextAnchor(description) {
|
|
133
133
|
return extractQuotedPhrases(description).length > 0 || extractMeaningfulDescriptionTokens(description).length > 0;
|
|
134
134
|
}
|
|
135
|
-
function
|
|
135
|
+
function regionFromOcrItems(items, kind) {
|
|
136
|
+
if (!items.length) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const sorted = [...items].sort((left, right) => {
|
|
140
|
+
if (left.y !== right.y)
|
|
141
|
+
return left.y - right.y;
|
|
142
|
+
return left.x - right.x;
|
|
143
|
+
});
|
|
144
|
+
const minX = Math.min(...sorted.map((item) => item.x));
|
|
145
|
+
const minY = Math.min(...sorted.map((item) => item.y));
|
|
146
|
+
const maxX = Math.max(...sorted.map((item) => item.x + item.width));
|
|
147
|
+
const maxY = Math.max(...sorted.map((item) => item.y + item.height));
|
|
148
|
+
const confidenceValues = sorted
|
|
149
|
+
.map((item) => Number(item.confidence))
|
|
150
|
+
.filter((value) => Number.isFinite(value));
|
|
151
|
+
const confidence = confidenceValues.length
|
|
152
|
+
? confidenceValues.reduce((sum, value) => sum + value, 0) / confidenceValues.length
|
|
153
|
+
: undefined;
|
|
154
|
+
return {
|
|
155
|
+
text: sorted.map((item) => item.text).join(" ").replace(/\s+/g, " ").trim(),
|
|
156
|
+
x: minX,
|
|
157
|
+
y: minY,
|
|
158
|
+
width: Math.max(1, maxX - minX),
|
|
159
|
+
height: Math.max(1, maxY - minY),
|
|
160
|
+
confidence,
|
|
161
|
+
kind,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function buildStructuredOcrRegions(candidates) {
|
|
165
|
+
if (!candidates.length) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const sorted = [...candidates].sort((left, right) => {
|
|
169
|
+
const leftCenterY = left.y + (left.height / 2);
|
|
170
|
+
const rightCenterY = right.y + (right.height / 2);
|
|
171
|
+
if (leftCenterY !== rightCenterY) {
|
|
172
|
+
return leftCenterY - rightCenterY;
|
|
173
|
+
}
|
|
174
|
+
return left.x - right.x;
|
|
175
|
+
});
|
|
176
|
+
const lines = [];
|
|
177
|
+
for (const candidate of sorted) {
|
|
178
|
+
const candidateCenterY = candidate.y + (candidate.height / 2);
|
|
179
|
+
const lastLine = lines[lines.length - 1];
|
|
180
|
+
if (!lastLine) {
|
|
181
|
+
lines.push([candidate]);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const referenceCenterY = lastLine.reduce((sum, item) => sum + item.y + (item.height / 2), 0) / lastLine.length;
|
|
185
|
+
const avgHeight = lastLine.reduce((sum, item) => sum + item.height, 0) / lastLine.length;
|
|
186
|
+
const maxDistance = Math.max(16, avgHeight * 0.75);
|
|
187
|
+
if (Math.abs(candidateCenterY - referenceCenterY) <= maxDistance) {
|
|
188
|
+
lastLine.push(candidate);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
lines.push([candidate]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const lineRegions = lines
|
|
195
|
+
.map((line) => regionFromOcrItems(line.sort((left, right) => left.x - right.x), "line"))
|
|
196
|
+
.filter(Boolean);
|
|
197
|
+
const blocks = [];
|
|
198
|
+
for (const line of lineRegions) {
|
|
199
|
+
const lastBlock = blocks[blocks.length - 1];
|
|
200
|
+
if (!lastBlock) {
|
|
201
|
+
blocks.push([line]);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const previous = lastBlock[lastBlock.length - 1];
|
|
205
|
+
const verticalGap = line.y - (previous.y + previous.height);
|
|
206
|
+
const horizontalOverlap = Math.max(0, Math.min(previous.x + previous.width, line.x + line.width) - Math.max(previous.x, line.x));
|
|
207
|
+
const overlapRatio = horizontalOverlap / Math.max(previous.width, line.width, 1);
|
|
208
|
+
const leftAlignmentDelta = Math.abs(previous.x - line.x);
|
|
209
|
+
if (verticalGap <= Math.max(22, previous.height * 1.6) && (overlapRatio >= 0.18 || leftAlignmentDelta <= 120)) {
|
|
210
|
+
lastBlock.push(line);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
blocks.push([line]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const blockRegions = blocks
|
|
217
|
+
.map((block) => {
|
|
218
|
+
const minX = Math.min(...block.map((line) => line.x));
|
|
219
|
+
const minY = Math.min(...block.map((line) => line.y));
|
|
220
|
+
const maxX = Math.max(...block.map((line) => line.x + line.width));
|
|
221
|
+
const maxY = Math.max(...block.map((line) => line.y + line.height));
|
|
222
|
+
const confidenceValues = block
|
|
223
|
+
.map((line) => Number(line.confidence))
|
|
224
|
+
.filter((value) => Number.isFinite(value));
|
|
225
|
+
const confidence = confidenceValues.length
|
|
226
|
+
? confidenceValues.reduce((sum, value) => sum + value, 0) / confidenceValues.length
|
|
227
|
+
: undefined;
|
|
228
|
+
return {
|
|
229
|
+
text: block.map((line) => line.text).join(" ").replace(/\s+/g, " ").trim(),
|
|
230
|
+
x: minX,
|
|
231
|
+
y: minY,
|
|
232
|
+
width: Math.max(1, maxX - minX),
|
|
233
|
+
height: Math.max(1, maxY - minY),
|
|
234
|
+
confidence,
|
|
235
|
+
kind: "block",
|
|
236
|
+
};
|
|
237
|
+
})
|
|
238
|
+
.filter((region) => region.text);
|
|
239
|
+
const wordRegions = candidates.map((candidate) => ({
|
|
240
|
+
text: candidate.text,
|
|
241
|
+
x: candidate.x,
|
|
242
|
+
y: candidate.y,
|
|
243
|
+
width: candidate.width,
|
|
244
|
+
height: candidate.height,
|
|
245
|
+
confidence: candidate.confidence,
|
|
246
|
+
kind: "word",
|
|
247
|
+
}));
|
|
248
|
+
const unique = new Set();
|
|
249
|
+
const deduped = [];
|
|
250
|
+
for (const region of [...blockRegions, ...lineRegions, ...wordRegions]) {
|
|
251
|
+
const key = `${normalizeText(region.text)}|${Math.round(region.x)}|${Math.round(region.y)}|${region.kind}`;
|
|
252
|
+
if (!region.text || unique.has(key)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
unique.add(key);
|
|
256
|
+
deduped.push(region);
|
|
257
|
+
}
|
|
258
|
+
return deduped;
|
|
259
|
+
}
|
|
260
|
+
function findOcrTextMatch(regions, description) {
|
|
136
261
|
const phrases = extractQuotedPhrases(description);
|
|
137
262
|
const tokens = extractMeaningfulDescriptionTokens(description);
|
|
138
263
|
const normalizedDescription = normalizeText(description || "");
|
|
@@ -140,10 +265,11 @@ function findOcrTextMatch(candidates, description) {
|
|
|
140
265
|
if (!phrases.length && !tokens.length) {
|
|
141
266
|
return null;
|
|
142
267
|
}
|
|
143
|
-
const scored =
|
|
144
|
-
.map((
|
|
145
|
-
const normalizedText = normalizeText(
|
|
268
|
+
const scored = regions
|
|
269
|
+
.map((region, index) => {
|
|
270
|
+
const normalizedText = normalizeText(region.text || "");
|
|
146
271
|
let score = 0;
|
|
272
|
+
let matchedTokens = 0;
|
|
147
273
|
for (const phrase of phrases) {
|
|
148
274
|
if (normalizedText.includes(phrase)) {
|
|
149
275
|
score += 120;
|
|
@@ -152,17 +278,27 @@ function findOcrTextMatch(candidates, description) {
|
|
|
152
278
|
for (const token of tokens) {
|
|
153
279
|
if (normalizedText.includes(token)) {
|
|
154
280
|
score += 18;
|
|
281
|
+
matchedTokens += 1;
|
|
155
282
|
}
|
|
156
283
|
}
|
|
284
|
+
if (tokens.length > 1 && matchedTokens === tokens.length) {
|
|
285
|
+
score += 36;
|
|
286
|
+
}
|
|
287
|
+
if (region.kind === "line") {
|
|
288
|
+
score += 8;
|
|
289
|
+
}
|
|
290
|
+
else if (region.kind === "block") {
|
|
291
|
+
score += 16;
|
|
292
|
+
}
|
|
157
293
|
if (wantsFirst) {
|
|
158
|
-
score += Math.max(0, 24 - Math.round(
|
|
294
|
+
score += Math.max(0, 24 - Math.round(region.y / 60));
|
|
159
295
|
score += Math.max(0, 12 - index);
|
|
160
296
|
}
|
|
161
|
-
if (
|
|
162
|
-
score += Math.round(
|
|
297
|
+
if (region.confidence) {
|
|
298
|
+
score += Math.round(region.confidence * 20);
|
|
163
299
|
}
|
|
164
300
|
return score > 0 ? {
|
|
165
|
-
|
|
301
|
+
region,
|
|
166
302
|
score,
|
|
167
303
|
} : null;
|
|
168
304
|
})
|
|
@@ -171,10 +307,10 @@ function findOcrTextMatch(candidates, description) {
|
|
|
171
307
|
if (right.score !== left.score) {
|
|
172
308
|
return right.score - left.score;
|
|
173
309
|
}
|
|
174
|
-
if (left.
|
|
175
|
-
return left.
|
|
310
|
+
if (left.region.y !== right.region.y) {
|
|
311
|
+
return left.region.y - right.region.y;
|
|
176
312
|
}
|
|
177
|
-
return left.
|
|
313
|
+
return left.region.x - right.region.x;
|
|
178
314
|
});
|
|
179
315
|
return scored[0] || null;
|
|
180
316
|
}
|
|
@@ -505,6 +641,15 @@ function parseStructuredActions(job) {
|
|
|
505
641
|
actions.push({ type: "list_files", path: filePath, limit });
|
|
506
642
|
continue;
|
|
507
643
|
}
|
|
644
|
+
if (type === "count_files") {
|
|
645
|
+
const filePath = asString(action.path) || "~";
|
|
646
|
+
const extensions = Array.isArray(action.extensions)
|
|
647
|
+
? action.extensions.map((item) => asString(item)?.toLowerCase().replace(/^\./, "")).filter(Boolean)
|
|
648
|
+
: undefined;
|
|
649
|
+
const recursive = typeof action.recursive === "boolean" ? action.recursive : undefined;
|
|
650
|
+
actions.push({ type: "count_files", path: filePath, extensions, recursive });
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
508
653
|
if (type === "run_shell" || type === "shell" || type === "terminal") {
|
|
509
654
|
const command = asString(action.command) || asString(action.cmd);
|
|
510
655
|
const cwd = asString(action.cwd);
|
|
@@ -535,6 +680,19 @@ function parseStructuredActions(job) {
|
|
|
535
680
|
}
|
|
536
681
|
continue;
|
|
537
682
|
}
|
|
683
|
+
if (type === "drag_visual_target" || type === "drag_target") {
|
|
684
|
+
const sourceDescription = asString(action.source_description) || asString(action.source) || asString(action.from);
|
|
685
|
+
const targetDescription = asString(action.target_description) || asString(action.target) || asString(action.to);
|
|
686
|
+
if (sourceDescription && targetDescription) {
|
|
687
|
+
actions.push({
|
|
688
|
+
type: "drag_visual_target",
|
|
689
|
+
source_description: sourceDescription,
|
|
690
|
+
target_description: targetDescription,
|
|
691
|
+
app: asString(action.app) || undefined,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
538
696
|
}
|
|
539
697
|
return actions;
|
|
540
698
|
}
|
|
@@ -752,6 +910,18 @@ export class NativeMacOSJobExecutor {
|
|
|
752
910
|
completionNotes.push(`Arquivos em ${action.path}:\n${listing}`);
|
|
753
911
|
continue;
|
|
754
912
|
}
|
|
913
|
+
if (action.type === "count_files") {
|
|
914
|
+
await reporter.progress(progressPercent, `Contando arquivos em ${action.path}`);
|
|
915
|
+
const counted = await this.countLocalFiles(action.path, action.extensions, action.recursive !== false);
|
|
916
|
+
completionNotes.push(`Encontrei ${counted.total} arquivo${counted.total === 1 ? "" : "s"} ${counted.extensionsLabel} em ${counted.path}.`);
|
|
917
|
+
resultPayload.file_count = {
|
|
918
|
+
total: counted.total,
|
|
919
|
+
path: counted.path,
|
|
920
|
+
extensions: counted.extensions,
|
|
921
|
+
recursive: counted.recursive,
|
|
922
|
+
};
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
755
925
|
if (action.type === "run_shell") {
|
|
756
926
|
await reporter.progress(progressPercent, `Rodando comando local: ${action.command}`);
|
|
757
927
|
const shellOutput = await this.runShellCommand(action.command, action.cwd);
|
|
@@ -839,15 +1009,15 @@ export class NativeMacOSJobExecutor {
|
|
|
839
1009
|
validated = true;
|
|
840
1010
|
}
|
|
841
1011
|
if (validated) {
|
|
842
|
-
const
|
|
1012
|
+
const region = ocrClick.region || null;
|
|
843
1013
|
resultPayload.last_click = {
|
|
844
1014
|
strategy: ocrClick.strategy || "local_ocr",
|
|
845
1015
|
score: ocrClick.score || null,
|
|
846
|
-
matched_text:
|
|
847
|
-
x:
|
|
848
|
-
y:
|
|
849
|
-
width:
|
|
850
|
-
height:
|
|
1016
|
+
matched_text: region?.text || null,
|
|
1017
|
+
x: region ? region.x + (region.width / 2) : null,
|
|
1018
|
+
y: region ? region.y + (region.height / 2) : null,
|
|
1019
|
+
width: region?.width || null,
|
|
1020
|
+
height: region?.height || null,
|
|
851
1021
|
};
|
|
852
1022
|
completionNotes.push(`Localizei e cliquei em ${targetDescription} por OCR local.`);
|
|
853
1023
|
clickSucceeded = true;
|
|
@@ -923,6 +1093,35 @@ export class NativeMacOSJobExecutor {
|
|
|
923
1093
|
}
|
|
924
1094
|
continue;
|
|
925
1095
|
}
|
|
1096
|
+
if (action.type === "drag_visual_target") {
|
|
1097
|
+
const dragApp = await this.resolveLikelyBrowserApp(action.app);
|
|
1098
|
+
if (dragApp) {
|
|
1099
|
+
await reporter.progress(progressPercent, `Trazendo ${dragApp} para frente antes do arraste`);
|
|
1100
|
+
await this.focusApp(dragApp);
|
|
1101
|
+
}
|
|
1102
|
+
else if (action.app) {
|
|
1103
|
+
await reporter.progress(progressPercent, `Trazendo ${action.app} para frente antes do arraste`);
|
|
1104
|
+
await this.focusApp(action.app);
|
|
1105
|
+
}
|
|
1106
|
+
await reporter.progress(progressPercent, `Capturando a tela para localizar ${action.source_description} e ${action.target_description}`);
|
|
1107
|
+
const screenshotPath = await this.takeScreenshot();
|
|
1108
|
+
const sourcePoint = await this.resolveVisualTargetPoint(job.job_id, screenshotPath, action.source_description, artifacts, "drag_source");
|
|
1109
|
+
const targetPoint = await this.resolveVisualTargetPoint(job.job_id, screenshotPath, action.target_description, artifacts, "drag_target");
|
|
1110
|
+
if (!sourcePoint) {
|
|
1111
|
+
throw new Error(`Nao consegui localizar ${action.source_description} com confianca suficiente para arrastar.`);
|
|
1112
|
+
}
|
|
1113
|
+
if (!targetPoint) {
|
|
1114
|
+
throw new Error(`Nao consegui localizar ${action.target_description} com confianca suficiente para concluir o arraste.`);
|
|
1115
|
+
}
|
|
1116
|
+
await reporter.progress(progressPercent, `Arrastando ${action.source_description} para ${action.target_description}`);
|
|
1117
|
+
await this.dragPoint(sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y);
|
|
1118
|
+
resultPayload.last_drag = {
|
|
1119
|
+
source: sourcePoint,
|
|
1120
|
+
target: targetPoint,
|
|
1121
|
+
};
|
|
1122
|
+
completionNotes.push(`Arrastei ${action.source_description} para ${action.target_description}.`);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
926
1125
|
await reporter.progress(progressPercent, `Abrindo ${action.url}${action.app ? ` em ${action.app}` : ""}`);
|
|
927
1126
|
await this.openUrl(action.url, action.app);
|
|
928
1127
|
await delay(1200);
|
|
@@ -1567,6 +1766,54 @@ post(.leftMouseUp)
|
|
|
1567
1766
|
`;
|
|
1568
1767
|
await this.runCommand("swift", ["-e", script, String(Math.round(x)), String(Math.round(y))]);
|
|
1569
1768
|
}
|
|
1769
|
+
async dragPoint(fromX, fromY, toX, toY) {
|
|
1770
|
+
const script = `
|
|
1771
|
+
import Cocoa
|
|
1772
|
+
import ApplicationServices
|
|
1773
|
+
|
|
1774
|
+
let fromX = Double(CommandLine.arguments[1]) ?? 0
|
|
1775
|
+
let fromY = Double(CommandLine.arguments[2]) ?? 0
|
|
1776
|
+
let toX = Double(CommandLine.arguments[3]) ?? 0
|
|
1777
|
+
let toY = Double(CommandLine.arguments[4]) ?? 0
|
|
1778
|
+
|
|
1779
|
+
let startPoint = CGPoint(x: fromX, y: fromY)
|
|
1780
|
+
let endPoint = CGPoint(x: toX, y: toY)
|
|
1781
|
+
let steps = max(8, Int(hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y) / 60.0))
|
|
1782
|
+
|
|
1783
|
+
func post(_ type: CGEventType, at point: CGPoint) {
|
|
1784
|
+
guard let event = CGEvent(mouseEventSource: nil, mouseType: type, mouseCursorPosition: point, mouseButton: .left) else {
|
|
1785
|
+
fputs("failed to create mouse event\\n", stderr)
|
|
1786
|
+
exit(1)
|
|
1787
|
+
}
|
|
1788
|
+
event.post(tap: .cghidEventTap)
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
post(.mouseMoved, at: startPoint)
|
|
1792
|
+
usleep(100000)
|
|
1793
|
+
post(.leftMouseDown, at: startPoint)
|
|
1794
|
+
usleep(90000)
|
|
1795
|
+
|
|
1796
|
+
for step in 1...steps {
|
|
1797
|
+
let progress = Double(step) / Double(steps)
|
|
1798
|
+
let point = CGPoint(
|
|
1799
|
+
x: startPoint.x + ((endPoint.x - startPoint.x) * progress),
|
|
1800
|
+
y: startPoint.y + ((endPoint.y - startPoint.y) * progress)
|
|
1801
|
+
)
|
|
1802
|
+
post(.leftMouseDragged, at: point)
|
|
1803
|
+
usleep(35000)
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
post(.leftMouseUp, at: endPoint)
|
|
1807
|
+
`;
|
|
1808
|
+
await this.runCommand("swift", [
|
|
1809
|
+
"-e",
|
|
1810
|
+
script,
|
|
1811
|
+
String(Math.round(fromX)),
|
|
1812
|
+
String(Math.round(fromY)),
|
|
1813
|
+
String(Math.round(toX)),
|
|
1814
|
+
String(Math.round(toY)),
|
|
1815
|
+
]);
|
|
1816
|
+
}
|
|
1570
1817
|
async runLocalOcr(filePath) {
|
|
1571
1818
|
const script = `
|
|
1572
1819
|
import Foundation
|
|
@@ -1656,9 +1903,27 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1656
1903
|
}
|
|
1657
1904
|
}
|
|
1658
1905
|
async tryLocalOcrClick(screenshotPath, description) {
|
|
1659
|
-
|
|
1906
|
+
const anchor = await this.resolveLocalOcrAnchor(screenshotPath, description);
|
|
1907
|
+
if (!anchor.region) {
|
|
1660
1908
|
return {
|
|
1661
1909
|
clicked: false,
|
|
1910
|
+
reason: anchor.reason,
|
|
1911
|
+
strategy: anchor.strategy,
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
const clickX = anchor.region.x + (anchor.region.width / 2);
|
|
1915
|
+
const clickY = anchor.region.y + (anchor.region.height / 2);
|
|
1916
|
+
await this.clickPoint(clickX, clickY);
|
|
1917
|
+
return {
|
|
1918
|
+
clicked: true,
|
|
1919
|
+
score: anchor.score,
|
|
1920
|
+
region: anchor.region,
|
|
1921
|
+
strategy: anchor.strategy,
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
async resolveLocalOcrAnchor(screenshotPath, description) {
|
|
1925
|
+
if (!descriptionLikelyHasTextAnchor(description)) {
|
|
1926
|
+
return {
|
|
1662
1927
|
reason: "A descricao nao traz ancora textual forte para OCR local.",
|
|
1663
1928
|
strategy: "local_ocr_skipped",
|
|
1664
1929
|
};
|
|
@@ -1666,27 +1931,69 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1666
1931
|
const candidates = await this.runLocalOcr(screenshotPath);
|
|
1667
1932
|
if (!candidates.length) {
|
|
1668
1933
|
return {
|
|
1669
|
-
clicked: false,
|
|
1670
1934
|
reason: "OCR local nao encontrou texto utilizavel na tela.",
|
|
1671
1935
|
strategy: "local_ocr_empty",
|
|
1672
1936
|
};
|
|
1673
1937
|
}
|
|
1674
|
-
const
|
|
1938
|
+
const regions = buildStructuredOcrRegions(candidates);
|
|
1939
|
+
const match = findOcrTextMatch(regions, description);
|
|
1675
1940
|
if (!match || match.score < 24) {
|
|
1676
1941
|
return {
|
|
1677
|
-
clicked: false,
|
|
1678
1942
|
reason: "OCR local nao encontrou texto suficientemente compativel com a descricao.",
|
|
1679
1943
|
strategy: "local_ocr_no_match",
|
|
1680
1944
|
};
|
|
1681
1945
|
}
|
|
1682
|
-
const clickX = match.candidate.x + (match.candidate.width / 2);
|
|
1683
|
-
const clickY = match.candidate.y + (match.candidate.height / 2);
|
|
1684
|
-
await this.clickPoint(clickX, clickY);
|
|
1685
1946
|
return {
|
|
1686
|
-
|
|
1947
|
+
region: match.region,
|
|
1687
1948
|
score: match.score,
|
|
1688
|
-
|
|
1689
|
-
|
|
1949
|
+
strategy: match.region.kind === "block" ? "structured_local_ocr_block" : "local_ocr",
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
async resolveVisualTargetPoint(jobId, screenshotPath, description, artifacts, purpose) {
|
|
1953
|
+
const ocrAnchor = await this.resolveLocalOcrAnchor(screenshotPath, description);
|
|
1954
|
+
if (ocrAnchor.region) {
|
|
1955
|
+
return {
|
|
1956
|
+
x: ocrAnchor.region.x + (ocrAnchor.region.width / 2),
|
|
1957
|
+
y: ocrAnchor.region.y + (ocrAnchor.region.height / 2),
|
|
1958
|
+
strategy: ocrAnchor.strategy || "local_ocr",
|
|
1959
|
+
matched_text: ocrAnchor.region.text,
|
|
1960
|
+
score: ocrAnchor.score || null,
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const uploadable = await this.buildUploadableImage(screenshotPath);
|
|
1964
|
+
const artifact = await this.uploadArtifactForJob(jobId, uploadable.path, {
|
|
1965
|
+
kind: "screenshot",
|
|
1966
|
+
mimeTypeOverride: uploadable.mimeType,
|
|
1967
|
+
fileNameOverride: uploadable.filename,
|
|
1968
|
+
metadata: {
|
|
1969
|
+
purpose,
|
|
1970
|
+
visible_in_chat: false,
|
|
1971
|
+
target: description,
|
|
1972
|
+
width: uploadable.dimensions?.width || undefined,
|
|
1973
|
+
height: uploadable.dimensions?.height || undefined,
|
|
1974
|
+
original_width: uploadable.originalDimensions?.width || undefined,
|
|
1975
|
+
original_height: uploadable.originalDimensions?.height || undefined,
|
|
1976
|
+
resized_for_upload: uploadable.resized,
|
|
1977
|
+
},
|
|
1978
|
+
});
|
|
1979
|
+
if (!artifact?.storage_path) {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
artifacts.push(artifact);
|
|
1983
|
+
const artifactMetadata = artifact.metadata || {};
|
|
1984
|
+
const width = Number(artifactMetadata.width || 0);
|
|
1985
|
+
const height = Number(artifactMetadata.height || 0);
|
|
1986
|
+
const originalWidth = Number(artifactMetadata.original_width || width || 0);
|
|
1987
|
+
const originalHeight = Number(artifactMetadata.original_height || height || 0);
|
|
1988
|
+
const location = await this.locateVisualTarget(jobId, artifact.storage_path, description, width, height, artifact.mime_type);
|
|
1989
|
+
if (!location?.found || typeof location.x !== "number" || typeof location.y !== "number") {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
return {
|
|
1993
|
+
x: width > 0 && originalWidth > 0 ? (location.x / width) * originalWidth : location.x,
|
|
1994
|
+
y: height > 0 && originalHeight > 0 ? (location.y / height) * originalHeight : location.y,
|
|
1995
|
+
strategy: "visual_locator",
|
|
1996
|
+
score: typeof location.confidence === "number" ? location.confidence : null,
|
|
1690
1997
|
};
|
|
1691
1998
|
}
|
|
1692
1999
|
async getImageDimensions(filePath) {
|
|
@@ -1798,6 +2105,55 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1798
2105
|
}));
|
|
1799
2106
|
return items.length > 0 ? items.join("\n") : "(pasta vazia)";
|
|
1800
2107
|
}
|
|
2108
|
+
async countLocalFiles(directoryPath, extensions, recursive = true) {
|
|
2109
|
+
const resolved = expandUserPath(directoryPath);
|
|
2110
|
+
const normalizedExtensions = Array.from(new Set((extensions || [])
|
|
2111
|
+
.map((extension) => String(extension || "").trim().toLowerCase().replace(/^\./, ""))
|
|
2112
|
+
.filter(Boolean)));
|
|
2113
|
+
const queue = [resolved];
|
|
2114
|
+
let total = 0;
|
|
2115
|
+
while (queue.length > 0) {
|
|
2116
|
+
const current = queue.shift();
|
|
2117
|
+
if (!current)
|
|
2118
|
+
continue;
|
|
2119
|
+
let entries;
|
|
2120
|
+
try {
|
|
2121
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
2122
|
+
}
|
|
2123
|
+
catch {
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
for (const entry of entries) {
|
|
2127
|
+
const entryPath = path.join(current, entry.name);
|
|
2128
|
+
if (entry.isDirectory()) {
|
|
2129
|
+
if (recursive) {
|
|
2130
|
+
queue.push(entryPath);
|
|
2131
|
+
}
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
if (!entry.isFile()) {
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
if (normalizedExtensions.length > 0) {
|
|
2138
|
+
const entryExtension = path.extname(entry.name).toLowerCase().replace(/^\./, "");
|
|
2139
|
+
if (!normalizedExtensions.includes(entryExtension)) {
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
total += 1;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const extensionsLabel = normalizedExtensions.length > 0
|
|
2147
|
+
? normalizedExtensions.map((extension) => `.${extension}`).join(", ")
|
|
2148
|
+
: "do tipo solicitado";
|
|
2149
|
+
return {
|
|
2150
|
+
total,
|
|
2151
|
+
path: directoryPath,
|
|
2152
|
+
extensions: normalizedExtensions,
|
|
2153
|
+
recursive,
|
|
2154
|
+
extensionsLabel,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
1801
2157
|
async runShellCommand(command, cwd) {
|
|
1802
2158
|
if (!isSafeShellCommand(command)) {
|
|
1803
2159
|
throw new Error("Nenhum comando shell foi informado para execucao local.");
|
|
@@ -1849,6 +2205,9 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1849
2205
|
if (action.type === "list_files") {
|
|
1850
2206
|
return `Arquivos listados em ${action.path}`;
|
|
1851
2207
|
}
|
|
2208
|
+
if (action.type === "count_files") {
|
|
2209
|
+
return `Arquivos contados em ${action.path}`;
|
|
2210
|
+
}
|
|
1852
2211
|
if (action.type === "run_shell") {
|
|
1853
2212
|
return `Comando ${action.command} executado no macOS`;
|
|
1854
2213
|
}
|
|
@@ -1858,6 +2217,9 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1858
2217
|
if (action.type === "click_visual_target") {
|
|
1859
2218
|
return `Clique guiado executado para ${action.description}`;
|
|
1860
2219
|
}
|
|
2220
|
+
if (action.type === "drag_visual_target") {
|
|
2221
|
+
return `Arraste guiado executado de ${action.source_description} para ${action.target_description}`;
|
|
2222
|
+
}
|
|
1861
2223
|
const target = humanizeUrl(action.url);
|
|
1862
2224
|
return `${target} foi aberto${action.app ? ` em ${action.app}` : ""}`;
|
|
1863
2225
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.5.
|
|
2
|
+
export const BRIDGE_VERSION = "0.5.6";
|
|
3
3
|
export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
|
|
4
4
|
export const DEFAULT_API_BASE_URL = "http://localhost:8000";
|
|
5
5
|
export const DEFAULT_POLL_INTERVAL_MS = 3000;
|