@leg3ndy/otto-bridge 0.7.5 → 0.8.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/dist/executors/native_macos.js +37 -8
- package/dist/macos_whatsapp_helper.js +81 -48
- package/dist/types.js +1 -1
- package/package.json +1 -1
|
@@ -1201,10 +1201,13 @@ export class NativeMacOSJobExecutor {
|
|
|
1201
1201
|
if (!verification.ok) {
|
|
1202
1202
|
throw new Error(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`);
|
|
1203
1203
|
}
|
|
1204
|
+
const afterSend = await this.readWhatsAppVisibleConversation(action.contact, Math.max(12, beforeSend.messages.length + 4)).catch(() => null);
|
|
1204
1205
|
resultPayload.whatsapp = {
|
|
1205
1206
|
action: "send_message",
|
|
1206
1207
|
contact: action.contact,
|
|
1207
1208
|
text_preview: clipText(action.text, 180),
|
|
1209
|
+
messages: afterSend?.messages || [],
|
|
1210
|
+
summary: afterSend?.summary || "",
|
|
1208
1211
|
};
|
|
1209
1212
|
completionNotes.push(`Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`);
|
|
1210
1213
|
continue;
|
|
@@ -2366,6 +2369,9 @@ function isVisible(element) {
|
|
|
2366
2369
|
|
|
2367
2370
|
function focusAndReplaceContent(element, value) {
|
|
2368
2371
|
element.focus();
|
|
2372
|
+
if (typeof element.click === "function") {
|
|
2373
|
+
element.click();
|
|
2374
|
+
}
|
|
2369
2375
|
const range = document.createRange();
|
|
2370
2376
|
range.selectNodeContents(element);
|
|
2371
2377
|
const selection = window.getSelection();
|
|
@@ -2373,7 +2379,14 @@ function focusAndReplaceContent(element, value) {
|
|
|
2373
2379
|
selection?.addRange(range);
|
|
2374
2380
|
document.execCommand("selectAll", false);
|
|
2375
2381
|
document.execCommand("delete", false);
|
|
2382
|
+
if ((element.innerText || element.textContent || "").trim().length > 0) {
|
|
2383
|
+
element.textContent = "";
|
|
2384
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: "", inputType: "deleteContentBackward" }));
|
|
2385
|
+
}
|
|
2376
2386
|
document.execCommand("insertText", false, value);
|
|
2387
|
+
if ((element.innerText || "").trim() !== value.trim()) {
|
|
2388
|
+
element.textContent = value;
|
|
2389
|
+
}
|
|
2377
2390
|
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: value, inputType: "insertText" }));
|
|
2378
2391
|
}
|
|
2379
2392
|
|
|
@@ -2418,18 +2431,36 @@ function isVisible(element) {
|
|
|
2418
2431
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
2419
2432
|
}
|
|
2420
2433
|
|
|
2434
|
+
function pickClickTarget(node) {
|
|
2435
|
+
const selectors = [
|
|
2436
|
+
'[role="gridcell"]',
|
|
2437
|
+
'[role="listitem"]',
|
|
2438
|
+
'[data-testid="cell-frame-container"]',
|
|
2439
|
+
'div[tabindex]',
|
|
2440
|
+
];
|
|
2441
|
+
for (const selector of selectors) {
|
|
2442
|
+
const candidate = node.closest(selector);
|
|
2443
|
+
if (candidate instanceof HTMLElement && isVisible(candidate)) {
|
|
2444
|
+
return candidate;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return node instanceof HTMLElement && isVisible(node) ? node : null;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2421
2450
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
2422
2451
|
.filter((node) => node instanceof HTMLElement)
|
|
2423
2452
|
.filter((node) => isVisible(node))
|
|
2424
2453
|
.map((node) => {
|
|
2425
2454
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
2455
|
+
const target = pickClickTarget(node);
|
|
2426
2456
|
let score = 0;
|
|
2427
2457
|
if (text === normalizedQuery) score += 160;
|
|
2428
2458
|
if (text.includes(normalizedQuery)) score += 100;
|
|
2429
2459
|
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
2430
|
-
|
|
2431
|
-
if (
|
|
2432
|
-
|
|
2460
|
+
if (target instanceof HTMLElement && target !== node) score += 20;
|
|
2461
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "gridcell") score += 30;
|
|
2462
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "listitem") score += 20;
|
|
2463
|
+
return { node, target, text, score };
|
|
2433
2464
|
})
|
|
2434
2465
|
.filter((item) => item.score > 0)
|
|
2435
2466
|
.sort((left, right) => right.score - left.score);
|
|
@@ -2439,12 +2470,10 @@ if (!titleNodes.length) {
|
|
|
2439
2470
|
}
|
|
2440
2471
|
|
|
2441
2472
|
const winner = titleNodes[0];
|
|
2442
|
-
const target = winner.
|
|
2473
|
+
const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
|
|
2443
2474
|
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
return { clicked: true };
|
|
2447
|
-
}
|
|
2475
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
2476
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
2448
2477
|
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2449
2478
|
return { clicked: true };
|
|
2450
2479
|
`, { contact }, this.getWhatsAppWebScriptOptions(false));
|
|
@@ -275,35 +275,37 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
275
275
|
|
|
276
276
|
const searchBox = candidates[0].node;
|
|
277
277
|
searchBox.focus();
|
|
278
|
+
if (typeof searchBox.click === "function") {
|
|
279
|
+
searchBox.click();
|
|
280
|
+
}
|
|
278
281
|
|
|
279
282
|
if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
|
|
280
283
|
searchBox.value = "";
|
|
281
284
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
282
285
|
searchBox.value = contact;
|
|
283
286
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
287
|
+
return { ok: true, nativeInputRequired: false };
|
|
284
288
|
} else {
|
|
285
|
-
|
|
286
|
-
const range = document.createRange();
|
|
287
|
-
range.selectNodeContents(searchBox);
|
|
288
|
-
selection?.removeAllRanges();
|
|
289
|
-
selection?.addRange(range);
|
|
290
|
-
document.execCommand("selectAll", false);
|
|
291
|
-
document.execCommand("delete", false);
|
|
292
|
-
document.execCommand("insertText", false, contact);
|
|
293
|
-
if ((searchBox.innerText || "").trim() !== contact.trim()) {
|
|
294
|
-
searchBox.textContent = contact;
|
|
295
|
-
}
|
|
289
|
+
searchBox.textContent = "";
|
|
296
290
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
291
|
+
return { ok: true, nativeInputRequired: true };
|
|
297
292
|
}
|
|
298
|
-
|
|
299
|
-
return { ok: true };
|
|
300
293
|
})()
|
|
301
294
|
`);
|
|
302
295
|
if (!(prepared.ok === true)) {
|
|
303
296
|
return false;
|
|
304
297
|
}
|
|
298
|
+
if (prepared.nativeInputRequired === true) {
|
|
299
|
+
await this.nativeClearText().catch(() => false);
|
|
300
|
+
const inserted = await this.nativeInsertText(contact).catch(() => false);
|
|
301
|
+
if (!inserted) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await delay(1000);
|
|
305
306
|
let enterTriggered = false;
|
|
306
|
-
let
|
|
307
|
+
let clickAttempts = 0;
|
|
308
|
+
const searchStartedAt = Date.now();
|
|
307
309
|
const deadline = Date.now() + 6_000;
|
|
308
310
|
while (Date.now() < deadline) {
|
|
309
311
|
await delay(500);
|
|
@@ -311,7 +313,8 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
311
313
|
(() => {
|
|
312
314
|
const query = ${JSON.stringify(contact)};
|
|
313
315
|
const enterTriggered = ${JSON.stringify(enterTriggered)};
|
|
314
|
-
const
|
|
316
|
+
const clickAttempts = ${JSON.stringify(clickAttempts)};
|
|
317
|
+
const allowEnterFallback = ${JSON.stringify(Date.now() - searchStartedAt >= 2_500)};
|
|
315
318
|
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
316
319
|
const normalizedQuery = normalize(query);
|
|
317
320
|
|
|
@@ -324,77 +327,107 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
324
327
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
function pickClickTarget(node) {
|
|
331
|
+
const selectors = [
|
|
332
|
+
'[role="gridcell"]',
|
|
333
|
+
'[role="listitem"]',
|
|
334
|
+
'[data-testid="cell-frame-container"]',
|
|
335
|
+
'div[tabindex]',
|
|
336
|
+
];
|
|
337
|
+
for (const selector of selectors) {
|
|
338
|
+
const candidate = node.closest(selector);
|
|
339
|
+
if (candidate instanceof HTMLElement && isVisible(candidate)) {
|
|
340
|
+
return candidate;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return node instanceof HTMLElement && isVisible(node) ? node : null;
|
|
344
|
+
}
|
|
345
|
+
|
|
327
346
|
function isConversationOpen() {
|
|
328
347
|
const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
|
|
329
348
|
.some((node) => isVisible(node));
|
|
330
349
|
const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
|
|
331
350
|
.some((node) => isVisible(node));
|
|
332
|
-
const headerMatch = Array.from(document.querySelectorAll('header span[title], header div[title], main span[title], main div[title]'))
|
|
351
|
+
const headerMatch = Array.from(document.querySelectorAll('main header span[title], main header div[title], [data-testid="conversation-info-header-chat-title"], header span[title], header div[title], main span[title], main div[title]'))
|
|
333
352
|
.filter((node) => node instanceof HTMLElement)
|
|
334
353
|
.filter((node) => isVisible(node))
|
|
335
354
|
.some((node) => {
|
|
336
355
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
337
|
-
return text === normalizedQuery
|
|
356
|
+
return text === normalizedQuery
|
|
357
|
+
|| text.includes(normalizedQuery)
|
|
358
|
+
|| (normalizedQuery.includes(text) && text.length >= 3);
|
|
338
359
|
});
|
|
339
|
-
|
|
360
|
+
const composerTargetMatch = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"] [contenteditable="true"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
|
|
361
|
+
.filter((node) => node instanceof HTMLElement)
|
|
362
|
+
.filter((node) => isVisible(node))
|
|
363
|
+
.some((node) => {
|
|
364
|
+
const text = normalize(
|
|
365
|
+
node.getAttribute("aria-label")
|
|
366
|
+
|| node.getAttribute("aria-placeholder")
|
|
367
|
+
|| node.getAttribute("placeholder")
|
|
368
|
+
|| ""
|
|
369
|
+
);
|
|
370
|
+
return text.includes(normalizedQuery);
|
|
371
|
+
});
|
|
372
|
+
return (headerMatch || composerTargetMatch) && (footerVisible || composerVisible);
|
|
340
373
|
}
|
|
341
374
|
|
|
342
375
|
if (isConversationOpen()) {
|
|
343
376
|
return { clicked: true, activatedBy: "already-open" };
|
|
344
377
|
}
|
|
345
378
|
|
|
346
|
-
const searchCandidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], input[role="textbox"], textarea, div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
|
|
347
|
-
.filter((node) => node instanceof HTMLElement)
|
|
348
|
-
.filter((node) => isVisible(node));
|
|
349
|
-
const searchBox = searchCandidates[0];
|
|
350
|
-
if (!enterTriggered && searchBox instanceof HTMLElement) {
|
|
351
|
-
searchBox.focus();
|
|
352
|
-
searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
353
|
-
searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
354
|
-
searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
355
|
-
return { clicked: false, enterTriggered: true };
|
|
356
|
-
}
|
|
357
|
-
|
|
358
379
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
359
380
|
.filter((node) => node instanceof HTMLElement)
|
|
360
381
|
.filter((node) => isVisible(node))
|
|
361
382
|
.map((node) => {
|
|
362
383
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
384
|
+
const target = pickClickTarget(node);
|
|
363
385
|
let score = 0;
|
|
364
386
|
if (text === normalizedQuery) score += 160;
|
|
365
387
|
if (text.includes(normalizedQuery)) score += 100;
|
|
366
388
|
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
389
|
+
if (target instanceof HTMLElement && target !== node) score += 20;
|
|
390
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "gridcell") score += 30;
|
|
391
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "listitem") score += 20;
|
|
392
|
+
return { node, target, score };
|
|
370
393
|
})
|
|
371
394
|
.filter((item) => item.score > 0)
|
|
372
395
|
.sort((left, right) => right.score - left.score);
|
|
373
396
|
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
397
|
+
if (titleNodes.length) {
|
|
398
|
+
if (clickAttempts >= 4) {
|
|
399
|
+
return { clicked: false };
|
|
400
|
+
}
|
|
377
401
|
|
|
378
|
-
|
|
379
|
-
|
|
402
|
+
const winner = titleNodes[0];
|
|
403
|
+
const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
|
|
404
|
+
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
405
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
406
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
407
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
408
|
+
return { clicked: false, clickAttempts: clickAttempts + 1 };
|
|
380
409
|
}
|
|
381
410
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
411
|
+
const searchCandidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], input[role="textbox"], textarea, div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
|
|
412
|
+
.filter((node) => node instanceof HTMLElement)
|
|
413
|
+
.filter((node) => isVisible(node));
|
|
414
|
+
const searchBox = searchCandidates[0];
|
|
415
|
+
if (allowEnterFallback && !enterTriggered && searchBox instanceof HTMLElement) {
|
|
416
|
+
searchBox.focus();
|
|
417
|
+
searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
418
|
+
searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
419
|
+
searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
420
|
+
return { clicked: false, enterTriggered: true };
|
|
388
421
|
}
|
|
389
|
-
|
|
390
|
-
return { clicked: false
|
|
422
|
+
|
|
423
|
+
return { clicked: false };
|
|
391
424
|
})()
|
|
392
425
|
`);
|
|
393
426
|
if (result.enterTriggered === true) {
|
|
394
427
|
enterTriggered = true;
|
|
395
428
|
}
|
|
396
|
-
if (result.
|
|
397
|
-
|
|
429
|
+
if (typeof result.clickAttempts === "number") {
|
|
430
|
+
clickAttempts = result.clickAttempts;
|
|
398
431
|
}
|
|
399
432
|
if (result.clicked === true) {
|
|
400
433
|
return true;
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.
|
|
2
|
+
export const BRIDGE_VERSION = "0.8.0";
|
|
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;
|