@leg3ndy/otto-bridge 0.6.8 → 0.6.10
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 +4 -4
- package/dist/executors/native_macos.js +5 -7
- package/dist/types.js +1 -1
- package/dist/whatsapp_background.js +135 -123
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
npm pack
|
|
36
|
-
npm install -g ./leg3ndy-otto-bridge-0.6.
|
|
36
|
+
npm install -g ./leg3ndy-otto-bridge-0.6.10.tgz
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
No `0.6.
|
|
39
|
+
No `0.6.10`, `playwright` deixa de ser opcional no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
|
|
40
40
|
|
|
41
41
|
## Publicacao
|
|
42
42
|
|
|
@@ -106,7 +106,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
106
106
|
|
|
107
107
|
### WhatsApp Web em background
|
|
108
108
|
|
|
109
|
-
Fluxo recomendado no `0.6.
|
|
109
|
+
Fluxo recomendado no `0.6.10`:
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
otto-bridge extensions --install whatsappweb
|
|
@@ -116,7 +116,7 @@ otto-bridge extensions --status whatsappweb
|
|
|
116
116
|
|
|
117
117
|
O setup agora abre o login do WhatsApp Web em um browser persistente do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
|
|
118
118
|
|
|
119
|
-
Contrato do `0.6.
|
|
119
|
+
Contrato do `0.6.10`:
|
|
120
120
|
|
|
121
121
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
122
122
|
- `otto-bridge run`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime estiver ativo, sem depender de uma aba aberta no Safari
|
|
@@ -1202,7 +1202,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1202
1202
|
contact: action.contact,
|
|
1203
1203
|
text_preview: clipText(action.text, 180),
|
|
1204
1204
|
};
|
|
1205
|
-
completionNotes.push(`Enviei
|
|
1205
|
+
completionNotes.push(`Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`);
|
|
1206
1206
|
continue;
|
|
1207
1207
|
}
|
|
1208
1208
|
if (action.type === "whatsapp_read_chat") {
|
|
@@ -2437,12 +2437,11 @@ if (!titleNodes.length) {
|
|
|
2437
2437
|
const winner = titleNodes[0];
|
|
2438
2438
|
const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
|
|
2439
2439
|
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
2440
|
-
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
2441
|
-
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
2442
|
-
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2443
2440
|
if (typeof target.click === "function") {
|
|
2444
2441
|
target.click();
|
|
2442
|
+
return { clicked: true };
|
|
2445
2443
|
}
|
|
2444
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2446
2445
|
return { clicked: true };
|
|
2447
2446
|
`, { contact }, this.getWhatsAppWebScriptOptions(false));
|
|
2448
2447
|
return Boolean(result?.clicked);
|
|
@@ -2508,12 +2507,11 @@ const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compo
|
|
|
2508
2507
|
const sendButton = sendCandidates[0];
|
|
2509
2508
|
if (sendButton instanceof HTMLElement) {
|
|
2510
2509
|
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
2511
|
-
sendButton.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
2512
|
-
sendButton.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
2513
|
-
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2514
2510
|
if (typeof sendButton.click === "function") {
|
|
2515
2511
|
sendButton.click();
|
|
2512
|
+
return { sent: true };
|
|
2516
2513
|
}
|
|
2514
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2517
2515
|
return { sent: true };
|
|
2518
2516
|
}
|
|
2519
2517
|
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.6.
|
|
2
|
+
export const BRIDGE_VERSION = "0.6.10";
|
|
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;
|
|
@@ -322,69 +322,78 @@ export class WhatsAppBackgroundBrowser {
|
|
|
322
322
|
}
|
|
323
323
|
async selectConversation(contact) {
|
|
324
324
|
await this.ensureReady();
|
|
325
|
-
const prepared = await this.withPage((page) =>
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
function focusAndReplaceContent(element, value) {
|
|
339
|
-
element.focus();
|
|
340
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
341
|
-
element.value = "";
|
|
342
|
-
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
|
343
|
-
element.value = value;
|
|
344
|
-
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
|
|
345
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
346
|
-
return;
|
|
325
|
+
const prepared = await this.withPage(async (page) => {
|
|
326
|
+
const focusResult = await page.evaluate(() => {
|
|
327
|
+
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
|
|
328
|
+
function isVisible(element) {
|
|
329
|
+
if (!(element instanceof HTMLElement))
|
|
330
|
+
return false;
|
|
331
|
+
const rect = element.getBoundingClientRect();
|
|
332
|
+
if (rect.width < 4 || rect.height < 4)
|
|
333
|
+
return false;
|
|
334
|
+
const style = window.getComputedStyle(element);
|
|
335
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
|
|
336
|
+
return false;
|
|
337
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
347
338
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
339
|
+
const candidates = 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"]'))
|
|
340
|
+
.filter((node) => node instanceof HTMLElement)
|
|
341
|
+
.filter((node) => isVisible(node))
|
|
342
|
+
.map((node) => {
|
|
343
|
+
const rect = node.getBoundingClientRect();
|
|
344
|
+
const label = normalize(node.getAttribute("aria-label") || node.getAttribute("data-testid") || node.textContent || "");
|
|
345
|
+
let score = 0;
|
|
346
|
+
if (rect.left < window.innerWidth * 0.45)
|
|
347
|
+
score += 30;
|
|
348
|
+
if (rect.top < 240)
|
|
349
|
+
score += 30;
|
|
350
|
+
if (label.includes("search") || label.includes("pesquisar") || label.includes("procure") || label.includes("chat list"))
|
|
351
|
+
score += 80;
|
|
352
|
+
if (node.closest('[data-testid="chat-list-search"], header'))
|
|
353
|
+
score += 25;
|
|
354
|
+
return { node, score };
|
|
355
|
+
})
|
|
356
|
+
.sort((left, right) => right.score - left.score);
|
|
357
|
+
if (!candidates.length) {
|
|
358
|
+
return { ok: false, reason: "Nao achei o campo de busca do WhatsApp Web." };
|
|
360
359
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const rect = node.getBoundingClientRect();
|
|
369
|
-
const label = normalize(node.getAttribute("aria-label") || node.getAttribute("data-testid") || node.textContent || "");
|
|
370
|
-
let score = 0;
|
|
371
|
-
if (rect.left < window.innerWidth * 0.45)
|
|
372
|
-
score += 30;
|
|
373
|
-
if (rect.top < 240)
|
|
374
|
-
score += 30;
|
|
375
|
-
if (label.includes("search") || label.includes("pesquisar") || label.includes("procure") || label.includes("chat list"))
|
|
376
|
-
score += 80;
|
|
377
|
-
if (node.closest('[data-testid="chat-list-search"], header'))
|
|
378
|
-
score += 25;
|
|
379
|
-
return { node, score };
|
|
380
|
-
})
|
|
381
|
-
.sort((left, right) => right.score - left.score);
|
|
382
|
-
if (!candidates.length) {
|
|
383
|
-
return { ok: false, reason: "Nao achei o campo de busca do WhatsApp Web." };
|
|
360
|
+
const searchBox = candidates[0].node;
|
|
361
|
+
searchBox.focus();
|
|
362
|
+
searchBox.click();
|
|
363
|
+
return { ok: true };
|
|
364
|
+
});
|
|
365
|
+
if (!focusResult.ok) {
|
|
366
|
+
return focusResult;
|
|
384
367
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
368
|
+
await page.keyboard?.press("Meta+A").catch(() => undefined);
|
|
369
|
+
await page.keyboard?.press("Backspace").catch(() => undefined);
|
|
370
|
+
await page.keyboard?.type(String(contact || ""), { delay: 25 }).catch(() => undefined);
|
|
371
|
+
const typedValue = await page.evaluate(() => {
|
|
372
|
+
function isVisible(element) {
|
|
373
|
+
if (!(element instanceof HTMLElement))
|
|
374
|
+
return false;
|
|
375
|
+
const rect = element.getBoundingClientRect();
|
|
376
|
+
if (rect.width < 4 || rect.height < 4)
|
|
377
|
+
return false;
|
|
378
|
+
const style = window.getComputedStyle(element);
|
|
379
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
|
|
380
|
+
return false;
|
|
381
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
382
|
+
}
|
|
383
|
+
const candidates = 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"]'))
|
|
384
|
+
.filter((node) => node instanceof HTMLElement)
|
|
385
|
+
.filter((node) => isVisible(node));
|
|
386
|
+
const field = candidates[0];
|
|
387
|
+
if (!field) {
|
|
388
|
+
return "";
|
|
389
|
+
}
|
|
390
|
+
if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) {
|
|
391
|
+
return field.value || "";
|
|
392
|
+
}
|
|
393
|
+
return field.innerText || field.textContent || "";
|
|
394
|
+
});
|
|
395
|
+
return { ok: true, typedValue: String(typedValue || "") };
|
|
396
|
+
});
|
|
388
397
|
if (!prepared.ok) {
|
|
389
398
|
return false;
|
|
390
399
|
}
|
|
@@ -430,12 +439,11 @@ export class WhatsAppBackgroundBrowser {
|
|
|
430
439
|
const winner = titleNodes[0];
|
|
431
440
|
const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
|
|
432
441
|
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
433
|
-
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
434
|
-
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
435
|
-
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
436
442
|
if (typeof target.click === "function") {
|
|
437
443
|
target.click();
|
|
444
|
+
return { clicked: true };
|
|
438
445
|
}
|
|
446
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
439
447
|
return { clicked: true };
|
|
440
448
|
}, contact));
|
|
441
449
|
if (result.clicked === true) {
|
|
@@ -446,70 +454,74 @@ export class WhatsAppBackgroundBrowser {
|
|
|
446
454
|
}
|
|
447
455
|
async sendMessage(text) {
|
|
448
456
|
await this.ensureReady();
|
|
449
|
-
const result = await this.withPage((page) =>
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
function clearAndFillComposer(element, nextValue) {
|
|
462
|
-
element.focus();
|
|
463
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
464
|
-
element.value = "";
|
|
465
|
-
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
|
466
|
-
element.value = nextValue;
|
|
467
|
-
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: nextValue }));
|
|
468
|
-
return;
|
|
457
|
+
const result = await this.withPage(async (page) => {
|
|
458
|
+
const focusedComposer = await page.evaluate(() => {
|
|
459
|
+
function isVisible(element) {
|
|
460
|
+
if (!(element instanceof HTMLElement))
|
|
461
|
+
return false;
|
|
462
|
+
const rect = element.getBoundingClientRect();
|
|
463
|
+
if (rect.width < 6 || rect.height < 6)
|
|
464
|
+
return false;
|
|
465
|
+
const style = window.getComputedStyle(element);
|
|
466
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
|
|
467
|
+
return false;
|
|
468
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
469
469
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
document.execCommand("delete", false);
|
|
477
|
-
document.execCommand("insertText", false, nextValue);
|
|
478
|
-
if ((element.innerText || "").trim() !== nextValue.trim()) {
|
|
479
|
-
element.textContent = nextValue;
|
|
470
|
+
const candidates = Array.from(document.querySelectorAll('footer div[contenteditable="true"], [data-testid="conversation-compose-box-input"], main footer [contenteditable="true"], footer textarea'))
|
|
471
|
+
.filter((node) => node instanceof HTMLElement)
|
|
472
|
+
.filter((node) => isVisible(node))
|
|
473
|
+
.sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
|
|
474
|
+
if (!candidates.length) {
|
|
475
|
+
return { ok: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
|
|
480
476
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return { sent: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
|
|
477
|
+
const composer = candidates[0];
|
|
478
|
+
composer.focus();
|
|
479
|
+
composer.click();
|
|
480
|
+
return { ok: true };
|
|
481
|
+
});
|
|
482
|
+
if (!focusedComposer.ok) {
|
|
483
|
+
return { sent: false, reason: String(focusedComposer.reason || "Nao achei o campo de mensagem do WhatsApp Web.") };
|
|
489
484
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
sendButton.click();
|
|
485
|
+
await page.keyboard?.press("Meta+A").catch(() => undefined);
|
|
486
|
+
await page.keyboard?.press("Backspace").catch(() => undefined);
|
|
487
|
+
await page.keyboard?.type(String(text || ""), { delay: 20 }).catch(() => undefined);
|
|
488
|
+
return page.evaluate((value) => {
|
|
489
|
+
function isVisible(element) {
|
|
490
|
+
if (!(element instanceof HTMLElement))
|
|
491
|
+
return false;
|
|
492
|
+
const rect = element.getBoundingClientRect();
|
|
493
|
+
if (rect.width < 6 || rect.height < 6)
|
|
494
|
+
return false;
|
|
495
|
+
const style = window.getComputedStyle(element);
|
|
496
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
|
|
497
|
+
return false;
|
|
498
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
505
499
|
}
|
|
500
|
+
const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"]'))
|
|
501
|
+
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
502
|
+
.filter((node) => node instanceof HTMLElement)
|
|
503
|
+
.filter((node) => isVisible(node));
|
|
504
|
+
const sendButton = sendCandidates[0];
|
|
505
|
+
if (sendButton instanceof HTMLElement) {
|
|
506
|
+
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
507
|
+
if (typeof sendButton.click === "function") {
|
|
508
|
+
sendButton.click();
|
|
509
|
+
return { sent: true };
|
|
510
|
+
}
|
|
511
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
512
|
+
return { sent: true };
|
|
513
|
+
}
|
|
514
|
+
const composerCandidates = Array.from(document.querySelectorAll('footer div[contenteditable="true"], [data-testid="conversation-compose-box-input"], main footer [contenteditable="true"], footer textarea'))
|
|
515
|
+
.filter((node) => node instanceof HTMLElement)
|
|
516
|
+
.filter((node) => isVisible(node))
|
|
517
|
+
.sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
|
|
518
|
+
const composer = composerCandidates[0];
|
|
519
|
+
composer?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
520
|
+
composer?.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
521
|
+
composer?.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
506
522
|
return { sent: true };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
510
|
-
composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
511
|
-
return { sent: true };
|
|
512
|
-
}, text));
|
|
523
|
+
}, text);
|
|
524
|
+
});
|
|
513
525
|
if (!result.sent) {
|
|
514
526
|
throw new Error(String(result.reason || "Nao consegui enviar a mensagem no WhatsApp Web."));
|
|
515
527
|
}
|
|
@@ -569,7 +581,7 @@ export class WhatsAppBackgroundBrowser {
|
|
|
569
581
|
if (!matched) {
|
|
570
582
|
return {
|
|
571
583
|
ok: false,
|
|
572
|
-
reason: "Nao consegui confirmar
|
|
584
|
+
reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
|
|
573
585
|
};
|
|
574
586
|
}
|
|
575
587
|
return { ok: true, reason: "" };
|