@leg3ndy/otto-bridge 0.6.8 → 0.6.9

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 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.8.tgz
36
+ npm install -g ./leg3ndy-otto-bridge-0.6.9.tgz
37
37
  ```
38
38
 
39
- No `0.6.8`, `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.
39
+ No `0.6.9`, `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.8`:
109
+ Fluxo recomendado no `0.6.9`:
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.8`:
119
+ Contrato do `0.6.9`:
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
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.6.8";
2
+ export const BRIDGE_VERSION = "0.6.9";
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) => page.evaluate((query) => {
326
- const normalize = (value) => String(value || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
327
- function isVisible(element) {
328
- if (!(element instanceof HTMLElement))
329
- return false;
330
- const rect = element.getBoundingClientRect();
331
- if (rect.width < 4 || rect.height < 4)
332
- return false;
333
- const style = window.getComputedStyle(element);
334
- if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
335
- return false;
336
- return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
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
- element.textContent = "";
349
- element.innerHTML = "";
350
- element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
351
- const selection = window.getSelection();
352
- const range = document.createRange();
353
- range.selectNodeContents(element);
354
- range.collapse(false);
355
- selection?.removeAllRanges();
356
- selection?.addRange(range);
357
- document.execCommand("insertText", false, value);
358
- if (normalize(element.innerText || element.textContent || "") !== normalize(value)) {
359
- element.textContent = value;
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
- element.dispatchEvent(new InputEvent("input", { bubbles: true, data: value, inputType: "insertText" }));
362
- element.dispatchEvent(new Event("change", { bubbles: true }));
363
- }
364
- const candidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
365
- .filter((node) => node instanceof HTMLElement)
366
- .filter((node) => isVisible(node))
367
- .map((node) => {
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
- focusAndReplaceContent(candidates[0].node, String(query || ""));
386
- return { ok: true };
387
- }, contact));
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
  }
@@ -446,70 +455,75 @@ export class WhatsAppBackgroundBrowser {
446
455
  }
447
456
  async sendMessage(text) {
448
457
  await this.ensureReady();
449
- const result = await this.withPage((page) => page.evaluate((value) => {
450
- function isVisible(element) {
451
- if (!(element instanceof HTMLElement))
452
- return false;
453
- const rect = element.getBoundingClientRect();
454
- if (rect.width < 6 || rect.height < 6)
455
- return false;
456
- const style = window.getComputedStyle(element);
457
- if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
458
- return false;
459
- return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
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;
458
+ const result = await this.withPage(async (page) => {
459
+ const focusedComposer = await page.evaluate(() => {
460
+ function isVisible(element) {
461
+ if (!(element instanceof HTMLElement))
462
+ return false;
463
+ const rect = element.getBoundingClientRect();
464
+ if (rect.width < 6 || rect.height < 6)
465
+ return false;
466
+ const style = window.getComputedStyle(element);
467
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
468
+ return false;
469
+ return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
469
470
  }
470
- const selection = window.getSelection();
471
- const range = document.createRange();
472
- range.selectNodeContents(element);
473
- selection?.removeAllRanges();
474
- selection?.addRange(range);
475
- document.execCommand("selectAll", false);
476
- document.execCommand("delete", false);
477
- document.execCommand("insertText", false, nextValue);
478
- if ((element.innerText || "").trim() !== nextValue.trim()) {
479
- element.textContent = nextValue;
471
+ const candidates = Array.from(document.querySelectorAll('footer div[contenteditable="true"], [data-testid="conversation-compose-box-input"], main footer [contenteditable="true"], footer textarea'))
472
+ .filter((node) => node instanceof HTMLElement)
473
+ .filter((node) => isVisible(node))
474
+ .sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
475
+ if (!candidates.length) {
476
+ return { ok: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
480
477
  }
481
- element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: nextValue }));
482
- }
483
- const candidates = Array.from(document.querySelectorAll('footer div[contenteditable="true"], [data-testid="conversation-compose-box-input"], main footer [contenteditable="true"], footer textarea'))
484
- .filter((node) => node instanceof HTMLElement)
485
- .filter((node) => isVisible(node))
486
- .sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
487
- if (!candidates.length) {
488
- return { sent: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
478
+ const composer = candidates[0];
479
+ composer.focus();
480
+ composer.click();
481
+ return { ok: true };
482
+ });
483
+ if (!focusedComposer.ok) {
484
+ return { sent: false, reason: String(focusedComposer.reason || "Nao achei o campo de mensagem do WhatsApp Web.") };
489
485
  }
490
- const composer = candidates[0];
491
- clearAndFillComposer(composer, String(value || ""));
492
- composer.click();
493
- 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"]'))
494
- .map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
495
- .filter((node) => node instanceof HTMLElement)
496
- .filter((node) => isVisible(node));
497
- const sendButton = sendCandidates[0];
498
- if (sendButton instanceof HTMLElement) {
499
- sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
500
- sendButton.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
501
- sendButton.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
502
- sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
503
- if (typeof sendButton.click === "function") {
504
- sendButton.click();
486
+ await page.keyboard?.press("Meta+A").catch(() => undefined);
487
+ await page.keyboard?.press("Backspace").catch(() => undefined);
488
+ await page.keyboard?.type(String(text || ""), { delay: 20 }).catch(() => undefined);
489
+ return page.evaluate((value) => {
490
+ function isVisible(element) {
491
+ if (!(element instanceof HTMLElement))
492
+ return false;
493
+ const rect = element.getBoundingClientRect();
494
+ if (rect.width < 6 || rect.height < 6)
495
+ return false;
496
+ const style = window.getComputedStyle(element);
497
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
498
+ return false;
499
+ return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
505
500
  }
501
+ 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"]'))
502
+ .map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
503
+ .filter((node) => node instanceof HTMLElement)
504
+ .filter((node) => isVisible(node));
505
+ const sendButton = sendCandidates[0];
506
+ if (sendButton instanceof HTMLElement) {
507
+ sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
508
+ sendButton.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
509
+ sendButton.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
510
+ sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
511
+ if (typeof sendButton.click === "function") {
512
+ sendButton.click();
513
+ }
514
+ return { sent: true };
515
+ }
516
+ const composerCandidates = Array.from(document.querySelectorAll('footer div[contenteditable="true"], [data-testid="conversation-compose-box-input"], main footer [contenteditable="true"], footer textarea'))
517
+ .filter((node) => node instanceof HTMLElement)
518
+ .filter((node) => isVisible(node))
519
+ .sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
520
+ const composer = composerCandidates[0];
521
+ composer?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
522
+ composer?.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
523
+ composer?.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
506
524
  return { sent: true };
507
- }
508
- composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
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));
525
+ }, text);
526
+ });
513
527
  if (!result.sent) {
514
528
  throw new Error(String(result.reason || "Nao consegui enviar a mensagem no WhatsApp Web."));
515
529
  }
@@ -569,7 +583,7 @@ export class WhatsAppBackgroundBrowser {
569
583
  if (!matched) {
570
584
  return {
571
585
  ok: false,
572
- reason: "Nao consegui confirmar visualmente a mensagem enviada no WhatsApp.",
586
+ reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
573
587
  };
574
588
  }
575
589
  return { ok: true, reason: "" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",