@leg3ndy/otto-bridge 0.7.3 → 0.7.5

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,12 +33,12 @@ 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.7.3.tgz
36
+ npm install -g ./leg3ndy-otto-bridge-0.7.4.tgz
37
37
  ```
38
38
 
39
- No `0.7.3`, `playwright` segue como dependencia obrigatoria 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.7.4`, `playwright` segue como dependencia obrigatoria 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
- No macOS, o `0.7.3` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
41
+ No macOS, o `0.7.4` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
42
42
 
43
43
  ## Publicacao
44
44
 
@@ -108,7 +108,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
108
108
 
109
109
  ### WhatsApp Web em background
110
110
 
111
- Fluxo recomendado no `0.7.3`:
111
+ Fluxo recomendado no `0.7.4`:
112
112
 
113
113
  ```bash
114
114
  otto-bridge extensions --install whatsappweb
@@ -118,13 +118,13 @@ otto-bridge extensions --status whatsappweb
118
118
 
119
119
  O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
120
120
 
121
- Contrato do `0.7.3`:
121
+ Contrato do `0.7.4`:
122
122
 
123
123
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
124
124
  - `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
125
125
  - ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
126
126
 
127
- ## Handoff rapido do 0.7.3
127
+ ## Handoff rapido do 0.7.4
128
128
 
129
129
  Ja fechado no codigo:
130
130
 
@@ -1190,10 +1190,14 @@ export class NativeMacOSJobExecutor {
1190
1190
  if (!selected) {
1191
1191
  throw new Error(`Nao consegui localizar a conversa do WhatsApp com ${action.contact}.`);
1192
1192
  }
1193
+ const beforeSend = await this.readWhatsAppVisibleConversation(action.contact, 8).catch(() => ({
1194
+ messages: [],
1195
+ summary: "",
1196
+ }));
1193
1197
  await reporter.progress(progressPercent, `Enviando a mensagem para ${action.contact} no WhatsApp`);
1194
1198
  await this.sendWhatsAppMessage(action.text);
1195
1199
  await delay(900);
1196
- const verification = await this.verifyWhatsAppLastMessage(action.text);
1200
+ const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
1197
1201
  if (!verification.ok) {
1198
1202
  throw new Error(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`);
1199
1203
  }
@@ -2619,26 +2623,51 @@ return { messages: messages.slice(-maxMessages) };
2619
2623
  };
2620
2624
  }
2621
2625
  async verifyWhatsAppLastMessage(expectedText) {
2626
+ return this.verifyWhatsAppLastMessageAgainstBaseline(expectedText);
2627
+ }
2628
+ async verifyWhatsAppLastMessageAgainstBaseline(expectedText, previousMessages) {
2622
2629
  const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
2623
2630
  if (backgroundBrowser) {
2624
- return backgroundBrowser.verifyLastMessage(expectedText);
2631
+ return backgroundBrowser.verifyLastMessage(expectedText, previousMessages);
2625
2632
  }
2626
- const chat = await this.readWhatsAppVisibleConversation("Contato", 6);
2633
+ const baseline = Array.isArray(previousMessages) ? previousMessages : [];
2634
+ const chat = await this.readWhatsAppVisibleConversation("Contato", Math.max(8, baseline.length + 2));
2627
2635
  if (!chat.messages.length) {
2628
2636
  return {
2629
2637
  ok: false,
2630
2638
  reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
2631
2639
  };
2632
2640
  }
2633
- const normalizedExpected = normalizeText(expectedText).slice(0, 60);
2634
- const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
2635
- if (!matched) {
2641
+ const normalizedExpected = normalizeText(expectedText).slice(0, 120);
2642
+ const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
2643
+ const beforeSignature = baseline.map(normalizeMessage).join("\n");
2644
+ const afterSignature = chat.messages.map(normalizeMessage).join("\n");
2645
+ const changed = beforeSignature !== afterSignature;
2646
+ const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
2647
+ const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
2648
+ const latest = chat.messages[chat.messages.length - 1] || null;
2649
+ const latestAuthor = normalizeText(latest?.author || "");
2650
+ const latestText = normalizeText(latest?.text || "");
2651
+ const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
2652
+ if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
2653
+ return { ok: true, reason: "" };
2654
+ }
2655
+ if (!changed) {
2656
+ return {
2657
+ ok: false,
2658
+ reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
2659
+ };
2660
+ }
2661
+ if (afterMatches <= beforeMatches) {
2636
2662
  return {
2637
2663
  ok: false,
2638
- reason: "Nao consegui confirmar visualmente a mensagem enviada no WhatsApp.",
2664
+ reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
2639
2665
  };
2640
2666
  }
2641
- return { ok: true, reason: "" };
2667
+ return {
2668
+ ok: false,
2669
+ reason: "Nao consegui confirmar visualmente a nova mensagem enviada no WhatsApp.",
2670
+ };
2642
2671
  }
2643
2672
  async takeScreenshot(targetPath) {
2644
2673
  const artifactsDir = path.join(os.homedir(), ".otto-bridge", "artifacts");
@@ -125,6 +125,21 @@ export class MacOSWhatsAppHelperRuntime {
125
125
  await this.start();
126
126
  await this.call("hide_background");
127
127
  }
128
+ async nativeInsertText(text) {
129
+ await this.start();
130
+ const result = await this.call("native_insert_text", { text });
131
+ return result?.ok === true;
132
+ }
133
+ async nativeClearText() {
134
+ await this.start();
135
+ const result = await this.call("native_clear_text");
136
+ return result?.ok === true;
137
+ }
138
+ async nativePressEnter() {
139
+ await this.start();
140
+ const result = await this.call("native_press_enter");
141
+ return result?.ok === true;
142
+ }
128
143
  async evaluate(script) {
129
144
  await this.start();
130
145
  return await this.call("evaluate_js", { script });
@@ -263,9 +278,9 @@ export class MacOSWhatsAppHelperRuntime {
263
278
 
264
279
  if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
265
280
  searchBox.value = "";
266
- searchBox.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
281
+ searchBox.dispatchEvent(new Event("input", { bubbles: true }));
267
282
  searchBox.value = contact;
268
- searchBox.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: contact }));
283
+ searchBox.dispatchEvent(new Event("input", { bubbles: true }));
269
284
  } else {
270
285
  const selection = window.getSelection();
271
286
  const range = document.createRange();
@@ -278,7 +293,7 @@ export class MacOSWhatsAppHelperRuntime {
278
293
  if ((searchBox.innerText || "").trim() !== contact.trim()) {
279
294
  searchBox.textContent = contact;
280
295
  }
281
- searchBox.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: contact }));
296
+ searchBox.dispatchEvent(new Event("input", { bubbles: true }));
282
297
  }
283
298
 
284
299
  return { ok: true };
@@ -287,12 +302,16 @@ export class MacOSWhatsAppHelperRuntime {
287
302
  if (!(prepared.ok === true)) {
288
303
  return false;
289
304
  }
305
+ let enterTriggered = false;
306
+ let clickFallbackTriggered = false;
290
307
  const deadline = Date.now() + 6_000;
291
308
  while (Date.now() < deadline) {
292
309
  await delay(500);
293
310
  const result = await this.evaluate(`
294
311
  (() => {
295
312
  const query = ${JSON.stringify(contact)};
313
+ const enterTriggered = ${JSON.stringify(enterTriggered)};
314
+ const clickFallbackTriggered = ${JSON.stringify(clickFallbackTriggered)};
296
315
  const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
297
316
  const normalizedQuery = normalize(query);
298
317
 
@@ -305,6 +324,37 @@ export class MacOSWhatsAppHelperRuntime {
305
324
  return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
306
325
  }
307
326
 
327
+ function isConversationOpen() {
328
+ const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
329
+ .some((node) => isVisible(node));
330
+ const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
331
+ .some((node) => isVisible(node));
332
+ const headerMatch = Array.from(document.querySelectorAll('header span[title], header div[title], main span[title], main div[title]'))
333
+ .filter((node) => node instanceof HTMLElement)
334
+ .filter((node) => isVisible(node))
335
+ .some((node) => {
336
+ const text = normalize(node.getAttribute("title") || node.textContent || "");
337
+ return text === normalizedQuery || text.includes(normalizedQuery);
338
+ });
339
+ return footerVisible || composerVisible || headerMatch;
340
+ }
341
+
342
+ if (isConversationOpen()) {
343
+ return { clicked: true, activatedBy: "already-open" };
344
+ }
345
+
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
+
308
358
  const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
309
359
  .filter((node) => node instanceof HTMLElement)
310
360
  .filter((node) => isVisible(node))
@@ -325,17 +375,27 @@ export class MacOSWhatsAppHelperRuntime {
325
375
  return { clicked: false };
326
376
  }
327
377
 
378
+ if (clickFallbackTriggered) {
379
+ return { clicked: false };
380
+ }
381
+
328
382
  const winner = titleNodes[0];
329
383
  const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
330
384
  target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
331
385
  if (typeof target.click === "function") {
332
386
  target.click();
333
- return { clicked: true };
387
+ return { clicked: false, clickFallbackTriggered: true };
334
388
  }
335
389
  target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
336
- return { clicked: true };
390
+ return { clicked: false, clickFallbackTriggered: true };
337
391
  })()
338
392
  `);
393
+ if (result.enterTriggered === true) {
394
+ enterTriggered = true;
395
+ }
396
+ if (result.clickFallbackTriggered === true) {
397
+ clickFallbackTriggered = true;
398
+ }
339
399
  if (result.clicked === true) {
340
400
  return true;
341
401
  }
@@ -398,50 +458,91 @@ export class MacOSWhatsAppHelperRuntime {
398
458
  }
399
459
 
400
460
  const composer = candidates[0].node;
401
- composer.focus();
402
461
  if (composer instanceof HTMLInputElement || composer instanceof HTMLTextAreaElement) {
462
+ composer.focus();
463
+ composer.click();
403
464
  composer.value = "";
404
- composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
465
+ composer.dispatchEvent(new Event("input", { bubbles: true }));
405
466
  composer.value = value;
406
- composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
407
- } else {
408
- const selection = window.getSelection();
409
- const range = document.createRange();
410
- range.selectNodeContents(composer);
411
- selection?.removeAllRanges();
412
- selection?.addRange(range);
413
- document.execCommand("selectAll", false);
414
- document.execCommand("delete", false);
415
- document.execCommand("insertText", false, value);
416
- if ((composer.innerText || "").trim() !== value.trim()) {
417
- composer.textContent = value;
467
+ composer.dispatchEvent(new Event("input", { bubbles: true }));
468
+ composer.click();
469
+
470
+ 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"]'))
471
+ .map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
472
+ .filter((node) => node instanceof HTMLElement)
473
+ .filter((node) => isVisible(node));
474
+
475
+ const sendButton = sendCandidates[0];
476
+ if (sendButton instanceof HTMLElement) {
477
+ sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
478
+ if (typeof sendButton.click === "function") {
479
+ sendButton.click();
480
+ return { sent: true };
481
+ }
482
+ sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
483
+ return { sent: true };
418
484
  }
419
- composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
485
+
486
+ composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
487
+ composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
488
+ composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
489
+ return { sent: true };
490
+ } else {
491
+ composer.focus();
492
+ composer.click();
493
+ return { nativeInputRequired: true };
494
+ }
495
+ })()
496
+ `);
497
+ if (result.nativeInputRequired === true) {
498
+ const cleared = await this.nativeClearText();
499
+ if (!cleared) {
500
+ throw new Error("O helper nativo do WhatsApp nao conseguiu limpar o rascunho da mensagem.");
501
+ }
502
+ const inserted = await this.nativeInsertText(text);
503
+ if (!inserted) {
504
+ throw new Error("O helper nativo do WhatsApp nao conseguiu digitar a mensagem.");
505
+ }
506
+ await delay(250);
507
+ const clickResult = await this.evaluate(`
508
+ (() => {
509
+ function isVisible(element) {
510
+ if (!(element instanceof HTMLElement)) return false;
511
+ const rect = element.getBoundingClientRect();
512
+ if (rect.width < 6 || rect.height < 6) return false;
513
+ const style = window.getComputedStyle(element);
514
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
515
+ return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
420
516
  }
421
- composer.click();
422
517
 
423
- 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"]'))
518
+ 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"], footer [data-icon="send-filled"], footer [data-icon="wds-ic-send-filled"]'))
424
519
  .map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
425
520
  .filter((node) => node instanceof HTMLElement)
426
521
  .filter((node) => isVisible(node));
427
522
 
428
523
  const sendButton = sendCandidates[0];
429
- if (sendButton instanceof HTMLElement) {
430
- sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
431
- if (typeof sendButton.click === "function") {
432
- sendButton.click();
433
- return { sent: true };
434
- }
435
- sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
436
- return { sent: true };
524
+ if (!(sendButton instanceof HTMLElement)) {
525
+ return { clicked: false };
437
526
  }
438
527
 
439
- composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
440
- composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
441
- composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
442
- return { sent: true };
528
+ sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
529
+ if (typeof sendButton.click === "function") {
530
+ sendButton.click();
531
+ return { clicked: true };
532
+ }
533
+ sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
534
+ return { clicked: true };
443
535
  })()
444
- `);
536
+ `);
537
+ if (clickResult.clicked === true) {
538
+ return;
539
+ }
540
+ const submitted = await this.nativePressEnter();
541
+ if (!submitted) {
542
+ throw new Error("O helper nativo do WhatsApp nao conseguiu enviar a mensagem.");
543
+ }
544
+ return;
545
+ }
445
546
  if (result.sent === true) {
446
547
  return;
447
548
  }
@@ -495,23 +596,45 @@ export class MacOSWhatsAppHelperRuntime {
495
596
  : "(sem mensagens visiveis na conversa)",
496
597
  };
497
598
  }
498
- async verifyLastMessage(expectedText) {
499
- const chat = await this.readVisibleConversation(6);
599
+ async verifyLastMessage(expectedText, previousMessages) {
600
+ const baseline = Array.isArray(previousMessages) ? previousMessages : [];
601
+ const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
500
602
  if (!chat.messages.length) {
501
603
  return {
502
604
  ok: false,
503
605
  reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
504
606
  };
505
607
  }
506
- const normalizedExpected = normalizeText(expectedText).slice(0, 60);
507
- const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
508
- if (!matched) {
608
+ const normalizedExpected = normalizeText(expectedText).slice(0, 120);
609
+ const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
610
+ const beforeSignature = baseline.map(normalizeMessage).join("\n");
611
+ const afterSignature = chat.messages.map(normalizeMessage).join("\n");
612
+ const changed = beforeSignature !== afterSignature;
613
+ const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
614
+ const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
615
+ const latest = chat.messages[chat.messages.length - 1] || null;
616
+ const latestAuthor = normalizeText(latest?.author || "");
617
+ const latestText = normalizeText(latest?.text || "");
618
+ const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
619
+ if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
620
+ return { ok: true, reason: "" };
621
+ }
622
+ if (!changed) {
623
+ return {
624
+ ok: false,
625
+ reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
626
+ };
627
+ }
628
+ if (afterMatches <= beforeMatches) {
509
629
  return {
510
630
  ok: false,
511
- reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
631
+ reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
512
632
  };
513
633
  }
514
- return { ok: true, reason: "" };
634
+ return {
635
+ ok: false,
636
+ reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
637
+ };
515
638
  }
516
639
  handleStdout(chunk) {
517
640
  this.stdoutBuffer += chunk;
@@ -24,6 +24,10 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
24
24
  app.setActivationPolicy(.accessory)
25
25
  window.title = "Otto WhatsApp Helper"
26
26
  window.isReleasedWhenClosed = false
27
+ window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
28
+ window.level = .normal
29
+ window.hasShadow = false
30
+ window.hidesOnDeactivate = false
27
31
  window.contentView = webView
28
32
  webView.navigationDelegate = self
29
33
  webView.customUserAgent = Self.chromeUserAgent
@@ -86,6 +90,13 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
86
90
  case "hide_background":
87
91
  hideBackground()
88
92
  sendResponse(id: id, result: ["background": true])
93
+ case "native_insert_text":
94
+ let text = String(describing: params["text"] ?? "")
95
+ sendResponse(id: id, result: ["ok": nativeInsertText(text)])
96
+ case "native_clear_text":
97
+ sendResponse(id: id, result: ["ok": nativeClearText()])
98
+ case "native_press_enter":
99
+ sendResponse(id: id, result: ["ok": nativePressEnter()])
89
100
  case "load_whatsapp":
90
101
  ensureWhatsAppLoaded()
91
102
  sendResponse(id: id, result: ["url": webView.url?.absoluteString ?? ""])
@@ -116,6 +127,8 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
116
127
  }
117
128
 
118
129
  private func showSetup() {
130
+ window.alphaValue = 1
131
+ window.ignoresMouseEvents = false
119
132
  if let screen = NSScreen.main {
120
133
  let frame = screen.visibleFrame
121
134
  let originX = frame.origin.x + max(0, (frame.width - 1320) / 2)
@@ -128,11 +141,59 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
128
141
  app.activate(ignoringOtherApps: true)
129
142
  }
130
143
 
144
+ private func prepareInteractionWindow() {
145
+ window.ignoresMouseEvents = true
146
+ window.alphaValue = 0
147
+ window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: false)
148
+ window.makeKeyAndOrderFront(nil)
149
+ app.activate(ignoringOtherApps: true)
150
+ }
151
+
131
152
  private func hideBackground() {
132
- window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: true)
153
+ window.ignoresMouseEvents = true
154
+ window.alphaValue = 0
155
+ window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: false)
133
156
  window.orderFrontRegardless()
134
157
  }
135
158
 
159
+ private func currentTextInputClient() -> NSTextInputClient? {
160
+ if let client = window.firstResponder as? NSTextInputClient {
161
+ return client
162
+ }
163
+ if let client = webView as? NSTextInputClient {
164
+ return client
165
+ }
166
+ return nil
167
+ }
168
+
169
+ private func nativeInsertText(_ text: String) -> Bool {
170
+ prepareInteractionWindow()
171
+ guard let client = currentTextInputClient() else {
172
+ return false
173
+ }
174
+ client.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0))
175
+ return true
176
+ }
177
+
178
+ private func nativeClearText() -> Bool {
179
+ prepareInteractionWindow()
180
+ guard let responder = window.firstResponder else {
181
+ return false
182
+ }
183
+ responder.selectAll(nil)
184
+ responder.doCommand(by: #selector(NSResponder.deleteBackward(_:)))
185
+ return true
186
+ }
187
+
188
+ private func nativePressEnter() -> Bool {
189
+ prepareInteractionWindow()
190
+ guard let responder = window.firstResponder else {
191
+ return false
192
+ }
193
+ responder.doCommand(by: #selector(NSResponder.insertNewline(_:)))
194
+ return true
195
+ }
196
+
136
197
  private func evaluateJavaScript(_ script: String, completion: @escaping (Any?, Error?) -> Void) {
137
198
  webView.evaluateJavaScript(script) { result, error in
138
199
  completion(result, error)
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.7.3";
2
+ export const BRIDGE_VERSION = "0.7.5";
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;
@@ -693,26 +693,48 @@ export class WhatsAppBackgroundBrowser {
693
693
  : "(sem mensagens visiveis na conversa)",
694
694
  };
695
695
  }
696
- async verifyLastMessage(expectedText) {
696
+ async verifyLastMessage(expectedText, previousMessages) {
697
697
  if (this.helperRuntime) {
698
- return await this.helperRuntime.verifyLastMessage(expectedText);
698
+ return await this.helperRuntime.verifyLastMessage(expectedText, previousMessages);
699
699
  }
700
- const chat = await this.readVisibleConversation(6);
700
+ const baseline = Array.isArray(previousMessages) ? previousMessages : [];
701
+ const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
701
702
  if (!chat.messages.length) {
702
703
  return {
703
704
  ok: false,
704
705
  reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
705
706
  };
706
707
  }
707
- const normalizedExpected = normalizeText(expectedText).slice(0, 60);
708
- const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
709
- if (!matched) {
708
+ const normalizedExpected = normalizeText(expectedText).slice(0, 120);
709
+ const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
710
+ const beforeSignature = baseline.map(normalizeMessage).join("\n");
711
+ const afterSignature = chat.messages.map(normalizeMessage).join("\n");
712
+ const changed = beforeSignature !== afterSignature;
713
+ const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
714
+ const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
715
+ const latest = chat.messages[chat.messages.length - 1] || null;
716
+ const latestAuthor = normalizeText(latest?.author || "");
717
+ const latestText = normalizeText(latest?.text || "");
718
+ const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
719
+ if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
720
+ return { ok: true, reason: "" };
721
+ }
722
+ if (!changed) {
710
723
  return {
711
724
  ok: false,
712
- reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
725
+ reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
713
726
  };
714
727
  }
715
- return { ok: true, reason: "" };
728
+ if (afterMatches <= beforeMatches) {
729
+ return {
730
+ ok: false,
731
+ reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
732
+ };
733
+ }
734
+ return {
735
+ ok: false,
736
+ reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
737
+ };
716
738
  }
717
739
  async ensureWhatsAppPage() {
718
740
  const page = this.page;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",