@leg3ndy/otto-bridge 0.6.10 → 0.7.2
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 +22 -5
- package/dist/executors/native_macos.js +54 -7
- package/dist/macos_whatsapp_helper.js +590 -0
- package/dist/macos_whatsapp_helper_source.js +176 -0
- package/dist/types.js +1 -1
- package/dist/whatsapp_background.js +159 -27
- package/dist/whatsapp_runtime_provider.js +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,10 +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.
|
|
36
|
+
npm install -g ./leg3ndy-otto-bridge-0.7.2.tgz
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
No `0.
|
|
39
|
+
No `0.7.2`, `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
|
+
|
|
41
|
+
No macOS, o `0.7.2` passa a preferir o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper agora sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. Se voce quiser forcar o runtime antigo com Chromium/Playwright, use `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
|
|
40
42
|
|
|
41
43
|
## Publicacao
|
|
42
44
|
|
|
@@ -106,7 +108,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
106
108
|
|
|
107
109
|
### WhatsApp Web em background
|
|
108
110
|
|
|
109
|
-
Fluxo recomendado no `0.
|
|
111
|
+
Fluxo recomendado no `0.7.2`:
|
|
110
112
|
|
|
111
113
|
```bash
|
|
112
114
|
otto-bridge extensions --install whatsappweb
|
|
@@ -114,14 +116,29 @@ otto-bridge extensions --setup whatsappweb
|
|
|
114
116
|
otto-bridge extensions --status whatsappweb
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
O setup agora abre o login do WhatsApp Web
|
|
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.
|
|
118
120
|
|
|
119
|
-
Contrato do `0.
|
|
121
|
+
Contrato do `0.7.2`:
|
|
120
122
|
|
|
121
123
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
122
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
|
|
123
125
|
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
124
126
|
|
|
127
|
+
## Handoff rapido do 0.7.2
|
|
128
|
+
|
|
129
|
+
Ja fechado no codigo:
|
|
130
|
+
|
|
131
|
+
- provider `macos-helper` dockless no macOS com fallback para `embedded-playwright`
|
|
132
|
+
- user-agent do helper ajustado para evitar bloqueio do WhatsApp por detecao de Safari/WebKit
|
|
133
|
+
- resultado final dos `device_job` agora e persistido como contexto mais forte para o proximo turno do Otto
|
|
134
|
+
- prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
|
|
135
|
+
|
|
136
|
+
Ainda precisa reteste em campo:
|
|
137
|
+
|
|
138
|
+
- fluxo completo do WhatsApp no helper `macos-helper`
|
|
139
|
+
- confirmacao de que o helper realmente passa do gate de compatibilidade do WhatsApp
|
|
140
|
+
- perguntas de follow-up como `o que voce mandou?` depois de uma acao local de mensagem
|
|
141
|
+
|
|
125
142
|
### Ver estado local
|
|
126
143
|
|
|
127
144
|
```bash
|
|
@@ -2452,7 +2452,9 @@ return { clicked: true };
|
|
|
2452
2452
|
await backgroundBrowser.sendMessage(text);
|
|
2453
2453
|
return;
|
|
2454
2454
|
}
|
|
2455
|
-
|
|
2455
|
+
let result = null;
|
|
2456
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2457
|
+
result = await this.runSafariJsonScript(`
|
|
2456
2458
|
const value = String(__input?.text || "");
|
|
2457
2459
|
function isVisible(element) {
|
|
2458
2460
|
if (!(element instanceof HTMLElement)) return false;
|
|
@@ -2463,6 +2465,10 @@ function isVisible(element) {
|
|
|
2463
2465
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
2464
2466
|
}
|
|
2465
2467
|
|
|
2468
|
+
function normalize(value) {
|
|
2469
|
+
return String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2466
2472
|
function clearAndFillComposer(element, nextValue) {
|
|
2467
2473
|
element.focus();
|
|
2468
2474
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
@@ -2486,17 +2492,53 @@ function clearAndFillComposer(element, nextValue) {
|
|
|
2486
2492
|
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: nextValue }));
|
|
2487
2493
|
}
|
|
2488
2494
|
|
|
2489
|
-
const
|
|
2490
|
-
|
|
2491
|
-
.
|
|
2492
|
-
|
|
2495
|
+
const uniqueTargets = new Set();
|
|
2496
|
+
const candidates = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea, div[contenteditable="true"][role="textbox"], div[contenteditable="true"][data-tab], textarea'))
|
|
2497
|
+
.map((node) => {
|
|
2498
|
+
if (!(node instanceof HTMLElement)) return null;
|
|
2499
|
+
const resolved = node.matches('[data-testid="conversation-compose-box-input"]')
|
|
2500
|
+
? node.querySelector('[contenteditable="true"], textarea')
|
|
2501
|
+
: node;
|
|
2502
|
+
return resolved instanceof HTMLElement ? resolved : null;
|
|
2503
|
+
})
|
|
2504
|
+
.filter((node) => {
|
|
2505
|
+
if (!(node instanceof HTMLElement) || uniqueTargets.has(node) || !isVisible(node)) {
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2508
|
+
uniqueTargets.add(node);
|
|
2509
|
+
return true;
|
|
2510
|
+
})
|
|
2511
|
+
.map((node) => {
|
|
2512
|
+
const rect = node.getBoundingClientRect();
|
|
2513
|
+
const label = normalize(
|
|
2514
|
+
node.getAttribute("aria-label")
|
|
2515
|
+
|| node.getAttribute("aria-placeholder")
|
|
2516
|
+
|| node.getAttribute("placeholder")
|
|
2517
|
+
|| node.getAttribute("data-testid")
|
|
2518
|
+
|| node.textContent
|
|
2519
|
+
|| ""
|
|
2520
|
+
);
|
|
2521
|
+
let score = 0;
|
|
2522
|
+
if (node.closest("footer")) score += 120;
|
|
2523
|
+
if (node.closest("main")) score += 60;
|
|
2524
|
+
if (rect.top > window.innerHeight * 0.55) score += 80;
|
|
2525
|
+
if (rect.left > window.innerWidth * 0.2) score += 30;
|
|
2526
|
+
if (label.includes("mensagem") || label.includes("message") || label.includes("digite") || label.includes("type")) score += 160;
|
|
2527
|
+
if (label.includes("search") || label.includes("pesquisar") || label.includes("procure") || label.includes("chat list")) score -= 240;
|
|
2528
|
+
if (node.closest('[data-testid="chat-list-search"], [role="search"]')) score -= 260;
|
|
2529
|
+
if (rect.top < 240) score -= 80;
|
|
2530
|
+
return { node, score };
|
|
2531
|
+
})
|
|
2532
|
+
.filter((item) => item.score > 0)
|
|
2533
|
+
.sort((left, right) => right.score - left.score || right.node.getBoundingClientRect().top - left.node.getBoundingClientRect().top);
|
|
2493
2534
|
|
|
2494
2535
|
if (!candidates.length) {
|
|
2495
2536
|
return { sent: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
|
|
2496
2537
|
}
|
|
2497
2538
|
|
|
2498
|
-
const composer = candidates[0];
|
|
2539
|
+
const composer = candidates[0].node;
|
|
2499
2540
|
clearAndFillComposer(composer, value);
|
|
2541
|
+
composer.focus();
|
|
2500
2542
|
composer.click();
|
|
2501
2543
|
|
|
2502
2544
|
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"]'))
|
|
@@ -2519,7 +2561,12 @@ composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter
|
|
|
2519
2561
|
composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
2520
2562
|
composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
2521
2563
|
return { sent: true };
|
|
2522
|
-
|
|
2564
|
+
`, { text }, this.getWhatsAppWebScriptOptions(false));
|
|
2565
|
+
if (result?.sent) {
|
|
2566
|
+
break;
|
|
2567
|
+
}
|
|
2568
|
+
await delay(500);
|
|
2569
|
+
}
|
|
2523
2570
|
if (!result?.sent) {
|
|
2524
2571
|
throw new Error(result?.reason || "Nao consegui enviar a mensagem no WhatsApp Web.");
|
|
2525
2572
|
}
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { getBridgeHomeDir } from "./config.js";
|
|
7
|
+
import { MACOS_WHATSAPP_HELPER_SWIFT_SOURCE } from "./macos_whatsapp_helper_source.js";
|
|
8
|
+
function delay(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
function normalizeText(value) {
|
|
12
|
+
return String(value || "")
|
|
13
|
+
.normalize("NFD")
|
|
14
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
async function runHelperCommand(command, args) {
|
|
19
|
+
return await new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
child.stdout.on("data", (chunk) => {
|
|
24
|
+
stdout += String(chunk);
|
|
25
|
+
});
|
|
26
|
+
child.stderr.on("data", (chunk) => {
|
|
27
|
+
stderr += String(chunk);
|
|
28
|
+
});
|
|
29
|
+
child.on("error", reject);
|
|
30
|
+
child.on("close", (code) => {
|
|
31
|
+
if (code === 0) {
|
|
32
|
+
resolve(stdout.trim() || stderr.trim());
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
reject(new Error(stderr.trim() || stdout.trim() || `${command} exited with code ${code}`));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function checkMacOSWhatsAppHelperAvailability() {
|
|
40
|
+
if (process.platform !== "darwin") {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "O helper dockless do WhatsApp so esta disponivel no macOS.",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await runHelperCommand("swift", ["--version"]);
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function ensureHelperScriptPath() {
|
|
58
|
+
const helperDir = path.join(getBridgeHomeDir(), "runtime");
|
|
59
|
+
await mkdir(helperDir, { recursive: true });
|
|
60
|
+
const helperPath = path.join(helperDir, "otto_whatsapp_helper.swift");
|
|
61
|
+
await writeFile(helperPath, MACOS_WHATSAPP_HELPER_SWIFT_SOURCE, "utf8");
|
|
62
|
+
return helperPath;
|
|
63
|
+
}
|
|
64
|
+
export class MacOSWhatsAppHelperRuntime {
|
|
65
|
+
options;
|
|
66
|
+
child = null;
|
|
67
|
+
stdoutBuffer = "";
|
|
68
|
+
pending = new Map();
|
|
69
|
+
started = false;
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.options = options;
|
|
72
|
+
}
|
|
73
|
+
async start() {
|
|
74
|
+
if (this.started && this.child) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const helperScriptPath = await ensureHelperScriptPath();
|
|
78
|
+
this.child = spawn("swift", [helperScriptPath], {
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
+
});
|
|
81
|
+
this.child.stdout.setEncoding("utf8");
|
|
82
|
+
this.child.stderr.setEncoding("utf8");
|
|
83
|
+
this.child.stdout.on("data", (chunk) => this.handleStdout(String(chunk)));
|
|
84
|
+
this.child.stderr.on("data", () => undefined);
|
|
85
|
+
this.child.on("exit", () => {
|
|
86
|
+
const error = new Error("Helper do WhatsApp foi encerrado.");
|
|
87
|
+
for (const pending of this.pending.values()) {
|
|
88
|
+
pending.reject(error);
|
|
89
|
+
}
|
|
90
|
+
this.pending.clear();
|
|
91
|
+
this.child = null;
|
|
92
|
+
this.started = false;
|
|
93
|
+
});
|
|
94
|
+
await this.waitForReady();
|
|
95
|
+
this.started = true;
|
|
96
|
+
await this.call("load_whatsapp");
|
|
97
|
+
if (this.options.background) {
|
|
98
|
+
await this.call("hide_background");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async close() {
|
|
102
|
+
const child = this.child;
|
|
103
|
+
if (!child) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await this.call("close");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
child.kill();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.child = null;
|
|
114
|
+
this.started = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async waitForTimeout(timeout) {
|
|
118
|
+
await delay(Math.max(0, timeout));
|
|
119
|
+
}
|
|
120
|
+
async showSetup() {
|
|
121
|
+
await this.start();
|
|
122
|
+
await this.call("show_setup");
|
|
123
|
+
}
|
|
124
|
+
async hideBackground() {
|
|
125
|
+
await this.start();
|
|
126
|
+
await this.call("hide_background");
|
|
127
|
+
}
|
|
128
|
+
async evaluate(script) {
|
|
129
|
+
await this.start();
|
|
130
|
+
return await this.call("evaluate_js", { script });
|
|
131
|
+
}
|
|
132
|
+
async getSessionState() {
|
|
133
|
+
const state = await this.evaluate(`
|
|
134
|
+
(() => {
|
|
135
|
+
function isVisible(element) {
|
|
136
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
137
|
+
const rect = element.getBoundingClientRect();
|
|
138
|
+
if (rect.width < 4 || rect.height < 4) return false;
|
|
139
|
+
const style = window.getComputedStyle(element);
|
|
140
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
141
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const qrVisible = Array.from(document.querySelectorAll('[data-testid="qrcode"], canvas[aria-label*="Scan"], canvas[aria-label*="scan"], div[data-ref] canvas'))
|
|
145
|
+
.some((node) => isVisible(node));
|
|
146
|
+
const paneVisible = Array.from(document.querySelectorAll('#pane-side, [data-testid="chat-list"]'))
|
|
147
|
+
.some((node) => isVisible(node));
|
|
148
|
+
const searchVisible = Array.from(document.querySelectorAll('[data-testid="chat-list-search"] [contenteditable="true"], div[contenteditable="true"][role="textbox"]'))
|
|
149
|
+
.some((node) => isVisible(node));
|
|
150
|
+
const composerVisible = Array.from(document.querySelectorAll('footer [contenteditable="true"], [data-testid="conversation-compose-box-input"]'))
|
|
151
|
+
.some((node) => isVisible(node));
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
title: document.title || "",
|
|
155
|
+
url: location.href || "",
|
|
156
|
+
qrVisible,
|
|
157
|
+
paneVisible,
|
|
158
|
+
searchVisible,
|
|
159
|
+
composerVisible,
|
|
160
|
+
connected: paneVisible || searchVisible || composerVisible,
|
|
161
|
+
};
|
|
162
|
+
})()
|
|
163
|
+
`);
|
|
164
|
+
return {
|
|
165
|
+
title: String(state.title || ""),
|
|
166
|
+
url: String(state.url || ""),
|
|
167
|
+
qrVisible: state.qrVisible === true,
|
|
168
|
+
paneVisible: state.paneVisible === true,
|
|
169
|
+
searchVisible: state.searchVisible === true,
|
|
170
|
+
composerVisible: state.composerVisible === true,
|
|
171
|
+
connected: state.connected === true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async waitForLogin(timeoutMs) {
|
|
175
|
+
const deadline = Date.now() + Math.max(10_000, timeoutMs);
|
|
176
|
+
let lastState = null;
|
|
177
|
+
while (Date.now() < deadline) {
|
|
178
|
+
lastState = await this.getSessionState();
|
|
179
|
+
if (lastState.connected) {
|
|
180
|
+
return lastState;
|
|
181
|
+
}
|
|
182
|
+
await delay(1500);
|
|
183
|
+
}
|
|
184
|
+
return lastState || await this.getSessionState();
|
|
185
|
+
}
|
|
186
|
+
async waitForStableSessionState(options) {
|
|
187
|
+
const timeoutMs = Math.max(2_000, Number(options?.timeoutMs || 15_000));
|
|
188
|
+
const pollIntervalMs = Math.max(250, Number(options?.pollIntervalMs || 1_000));
|
|
189
|
+
const qrStabilityWindowMs = Math.max(pollIntervalMs, Number(options?.qrStabilityWindowMs || 4_000));
|
|
190
|
+
const deadline = Date.now() + timeoutMs;
|
|
191
|
+
let lastState = null;
|
|
192
|
+
let qrVisibleSince = null;
|
|
193
|
+
while (Date.now() < deadline) {
|
|
194
|
+
lastState = await this.getSessionState();
|
|
195
|
+
if (lastState.connected) {
|
|
196
|
+
return lastState;
|
|
197
|
+
}
|
|
198
|
+
if (lastState.qrVisible) {
|
|
199
|
+
if (qrVisibleSince === null) {
|
|
200
|
+
qrVisibleSince = Date.now();
|
|
201
|
+
}
|
|
202
|
+
else if (Date.now() - qrVisibleSince >= qrStabilityWindowMs) {
|
|
203
|
+
return lastState;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
qrVisibleSince = null;
|
|
208
|
+
}
|
|
209
|
+
await delay(pollIntervalMs);
|
|
210
|
+
}
|
|
211
|
+
return lastState || await this.getSessionState();
|
|
212
|
+
}
|
|
213
|
+
async ensureReady() {
|
|
214
|
+
const state = await this.waitForStableSessionState({
|
|
215
|
+
timeoutMs: 45_000,
|
|
216
|
+
qrStabilityWindowMs: 10_000,
|
|
217
|
+
});
|
|
218
|
+
if (state.connected) {
|
|
219
|
+
return state;
|
|
220
|
+
}
|
|
221
|
+
if (state.qrVisible) {
|
|
222
|
+
throw new Error("WhatsApp Web ainda nao esta conectado nesta maquina. Rode `otto-bridge extensions --setup whatsappweb`, escaneie o QR code e depois `otto-bridge extensions --status whatsappweb`.");
|
|
223
|
+
}
|
|
224
|
+
throw new Error("Nao consegui confirmar uma sessao pronta do WhatsApp Web neste helper macOS em background.");
|
|
225
|
+
}
|
|
226
|
+
async selectConversation(contact) {
|
|
227
|
+
await this.ensureReady();
|
|
228
|
+
const prepared = await this.evaluate(`
|
|
229
|
+
(() => {
|
|
230
|
+
const contact = ${JSON.stringify(contact)};
|
|
231
|
+
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
232
|
+
|
|
233
|
+
function isVisible(element) {
|
|
234
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
235
|
+
const rect = element.getBoundingClientRect();
|
|
236
|
+
if (rect.width < 4 || rect.height < 4) return false;
|
|
237
|
+
const style = window.getComputedStyle(element);
|
|
238
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
239
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
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"]'))
|
|
243
|
+
.filter((node) => node instanceof HTMLElement)
|
|
244
|
+
.filter((node) => isVisible(node))
|
|
245
|
+
.map((node) => {
|
|
246
|
+
const rect = node.getBoundingClientRect();
|
|
247
|
+
const label = normalize(node.getAttribute("aria-label") || node.getAttribute("data-testid") || node.textContent || "");
|
|
248
|
+
let score = 0;
|
|
249
|
+
if (rect.left < window.innerWidth * 0.45) score += 30;
|
|
250
|
+
if (rect.top < 240) score += 30;
|
|
251
|
+
if (label.includes("search") || label.includes("pesquisar") || label.includes("procure") || label.includes("chat list")) score += 80;
|
|
252
|
+
if (node.closest('[data-testid="chat-list-search"], header')) score += 25;
|
|
253
|
+
return { node, score };
|
|
254
|
+
})
|
|
255
|
+
.sort((left, right) => right.score - left.score);
|
|
256
|
+
|
|
257
|
+
if (!candidates.length) {
|
|
258
|
+
return { ok: false, reason: "Nao achei o campo de busca do WhatsApp Web." };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const searchBox = candidates[0].node;
|
|
262
|
+
searchBox.focus();
|
|
263
|
+
|
|
264
|
+
if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
|
|
265
|
+
searchBox.value = "";
|
|
266
|
+
searchBox.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
|
267
|
+
searchBox.value = contact;
|
|
268
|
+
searchBox.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: contact }));
|
|
269
|
+
} else {
|
|
270
|
+
const selection = window.getSelection();
|
|
271
|
+
const range = document.createRange();
|
|
272
|
+
range.selectNodeContents(searchBox);
|
|
273
|
+
selection?.removeAllRanges();
|
|
274
|
+
selection?.addRange(range);
|
|
275
|
+
document.execCommand("selectAll", false);
|
|
276
|
+
document.execCommand("delete", false);
|
|
277
|
+
document.execCommand("insertText", false, contact);
|
|
278
|
+
if ((searchBox.innerText || "").trim() !== contact.trim()) {
|
|
279
|
+
searchBox.textContent = contact;
|
|
280
|
+
}
|
|
281
|
+
searchBox.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: contact }));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { ok: true };
|
|
285
|
+
})()
|
|
286
|
+
`);
|
|
287
|
+
if (!(prepared.ok === true)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const deadline = Date.now() + 6_000;
|
|
291
|
+
while (Date.now() < deadline) {
|
|
292
|
+
await delay(500);
|
|
293
|
+
const result = await this.evaluate(`
|
|
294
|
+
(() => {
|
|
295
|
+
const query = ${JSON.stringify(contact)};
|
|
296
|
+
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
297
|
+
const normalizedQuery = normalize(query);
|
|
298
|
+
|
|
299
|
+
function isVisible(element) {
|
|
300
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
301
|
+
const rect = element.getBoundingClientRect();
|
|
302
|
+
if (rect.width < 6 || rect.height < 6) return false;
|
|
303
|
+
const style = window.getComputedStyle(element);
|
|
304
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
305
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
309
|
+
.filter((node) => node instanceof HTMLElement)
|
|
310
|
+
.filter((node) => isVisible(node))
|
|
311
|
+
.map((node) => {
|
|
312
|
+
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
313
|
+
let score = 0;
|
|
314
|
+
if (text === normalizedQuery) score += 160;
|
|
315
|
+
if (text.includes(normalizedQuery)) score += 100;
|
|
316
|
+
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
317
|
+
const container = node.closest('[data-testid="cell-frame-container"], [role="listitem"], [role="gridcell"], div[tabindex]');
|
|
318
|
+
if (container instanceof HTMLElement && isVisible(container)) score += 20;
|
|
319
|
+
return { node, container, score };
|
|
320
|
+
})
|
|
321
|
+
.filter((item) => item.score > 0)
|
|
322
|
+
.sort((left, right) => right.score - left.score);
|
|
323
|
+
|
|
324
|
+
if (!titleNodes.length) {
|
|
325
|
+
return { clicked: false };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const winner = titleNodes[0];
|
|
329
|
+
const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
|
|
330
|
+
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
331
|
+
if (typeof target.click === "function") {
|
|
332
|
+
target.click();
|
|
333
|
+
return { clicked: true };
|
|
334
|
+
}
|
|
335
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
336
|
+
return { clicked: true };
|
|
337
|
+
})()
|
|
338
|
+
`);
|
|
339
|
+
if (result.clicked === true) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
async sendMessage(text) {
|
|
346
|
+
await this.ensureReady();
|
|
347
|
+
let result = null;
|
|
348
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
349
|
+
result = await this.evaluate(`
|
|
350
|
+
(() => {
|
|
351
|
+
const value = ${JSON.stringify(text)};
|
|
352
|
+
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
353
|
+
|
|
354
|
+
function isVisible(element) {
|
|
355
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
356
|
+
const rect = element.getBoundingClientRect();
|
|
357
|
+
if (rect.width < 6 || rect.height < 6) return false;
|
|
358
|
+
const style = window.getComputedStyle(element);
|
|
359
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
360
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const uniqueTargets = new Set();
|
|
364
|
+
const candidates = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea, div[contenteditable="true"][role="textbox"], div[contenteditable="true"][data-tab], textarea'))
|
|
365
|
+
.map((node) => {
|
|
366
|
+
if (!(node instanceof HTMLElement)) return null;
|
|
367
|
+
const resolved = node.matches('[data-testid="conversation-compose-box-input"]')
|
|
368
|
+
? node.querySelector('[contenteditable="true"], textarea')
|
|
369
|
+
: node;
|
|
370
|
+
return resolved instanceof HTMLElement ? resolved : null;
|
|
371
|
+
})
|
|
372
|
+
.filter((node) => {
|
|
373
|
+
if (!(node instanceof HTMLElement) || uniqueTargets.has(node) || !isVisible(node)) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
uniqueTargets.add(node);
|
|
377
|
+
return true;
|
|
378
|
+
})
|
|
379
|
+
.map((node) => {
|
|
380
|
+
const rect = node.getBoundingClientRect();
|
|
381
|
+
const label = normalize(node.getAttribute("aria-label") || node.getAttribute("aria-placeholder") || node.getAttribute("placeholder") || node.getAttribute("data-testid") || node.textContent || "");
|
|
382
|
+
let score = 0;
|
|
383
|
+
if (node.closest("footer")) score += 120;
|
|
384
|
+
if (node.closest("main")) score += 60;
|
|
385
|
+
if (rect.top > window.innerHeight * 0.55) score += 80;
|
|
386
|
+
if (rect.left > window.innerWidth * 0.2) score += 30;
|
|
387
|
+
if (label.includes("mensagem") || label.includes("message") || label.includes("digite") || label.includes("type")) score += 160;
|
|
388
|
+
if (label.includes("search") || label.includes("pesquisar") || label.includes("procure") || label.includes("chat list")) score -= 240;
|
|
389
|
+
if (node.closest('[data-testid="chat-list-search"], [role="search"]')) score -= 260;
|
|
390
|
+
if (rect.top < 240) score -= 80;
|
|
391
|
+
return { node, score };
|
|
392
|
+
})
|
|
393
|
+
.filter((item) => item.score > 0)
|
|
394
|
+
.sort((left, right) => right.score - left.score || right.node.getBoundingClientRect().top - left.node.getBoundingClientRect().top);
|
|
395
|
+
|
|
396
|
+
if (!candidates.length) {
|
|
397
|
+
return { sent: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const composer = candidates[0].node;
|
|
401
|
+
composer.focus();
|
|
402
|
+
if (composer instanceof HTMLInputElement || composer instanceof HTMLTextAreaElement) {
|
|
403
|
+
composer.value = "";
|
|
404
|
+
composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
|
405
|
+
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;
|
|
418
|
+
}
|
|
419
|
+
composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
|
|
420
|
+
}
|
|
421
|
+
composer.click();
|
|
422
|
+
|
|
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"]'))
|
|
424
|
+
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
425
|
+
.filter((node) => node instanceof HTMLElement)
|
|
426
|
+
.filter((node) => isVisible(node));
|
|
427
|
+
|
|
428
|
+
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 };
|
|
437
|
+
}
|
|
438
|
+
|
|
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 };
|
|
443
|
+
})()
|
|
444
|
+
`);
|
|
445
|
+
if (result.sent === true) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
await delay(500);
|
|
449
|
+
}
|
|
450
|
+
throw new Error(String(result?.reason || "Nao consegui enviar a mensagem no WhatsApp Web."));
|
|
451
|
+
}
|
|
452
|
+
async readVisibleConversation(limit) {
|
|
453
|
+
await this.ensureReady();
|
|
454
|
+
const result = await this.evaluate(`
|
|
455
|
+
(() => {
|
|
456
|
+
const maxMessages = ${Math.max(1, Number(limit || 12))};
|
|
457
|
+
|
|
458
|
+
function isVisible(element) {
|
|
459
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
460
|
+
const rect = element.getBoundingClientRect();
|
|
461
|
+
if (rect.width < 6 || rect.height < 6) return false;
|
|
462
|
+
const style = window.getComputedStyle(element);
|
|
463
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
464
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const containers = Array.from(document.querySelectorAll('[data-testid="msg-container"], div[data-id]'))
|
|
468
|
+
.filter((node) => node instanceof HTMLElement)
|
|
469
|
+
.filter((node) => isVisible(node));
|
|
470
|
+
|
|
471
|
+
const messages = containers.map((element) => {
|
|
472
|
+
const prePlain = element.querySelector('[data-pre-plain-text]')?.getAttribute("data-pre-plain-text") || "";
|
|
473
|
+
const authorMatch = prePlain.match(/\\]\\s*([^:]+):/);
|
|
474
|
+
const author = authorMatch?.[1]?.trim() || (element.getAttribute("data-testid")?.includes("out") ? "Voce" : "Contato");
|
|
475
|
+
const text = (element.innerText || "").trim().replace(/\\n{2,}/g, "\\n");
|
|
476
|
+
return { author, text };
|
|
477
|
+
}).filter((item) => item.text);
|
|
478
|
+
|
|
479
|
+
return { messages: messages.slice(-maxMessages) };
|
|
480
|
+
})()
|
|
481
|
+
`);
|
|
482
|
+
const rawMessages = Array.isArray(result.messages)
|
|
483
|
+
? result.messages
|
|
484
|
+
: [];
|
|
485
|
+
const messages = rawMessages
|
|
486
|
+
.map((item) => ({
|
|
487
|
+
author: String(item.author || "Contato").trim().slice(0, 80),
|
|
488
|
+
text: String(item.text || "").trim().slice(0, 500),
|
|
489
|
+
}))
|
|
490
|
+
.filter((item) => item.text);
|
|
491
|
+
return {
|
|
492
|
+
messages,
|
|
493
|
+
summary: messages.length
|
|
494
|
+
? messages.map((item) => `${item.author}: ${item.text}`).join("\n")
|
|
495
|
+
: "(sem mensagens visiveis na conversa)",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async verifyLastMessage(expectedText) {
|
|
499
|
+
const chat = await this.readVisibleConversation(6);
|
|
500
|
+
if (!chat.messages.length) {
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const normalizedExpected = normalizeText(expectedText).slice(0, 60);
|
|
507
|
+
const matched = chat.messages.some((item) => normalizeText(item.text).includes(normalizedExpected));
|
|
508
|
+
if (!matched) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
reason: "Nao consegui confirmar na conversa do WhatsApp se a mensagem foi enviada.",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return { ok: true, reason: "" };
|
|
515
|
+
}
|
|
516
|
+
handleStdout(chunk) {
|
|
517
|
+
this.stdoutBuffer += chunk;
|
|
518
|
+
while (true) {
|
|
519
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
520
|
+
if (newlineIndex === -1) {
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
524
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
525
|
+
if (!line) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
let parsed = null;
|
|
529
|
+
try {
|
|
530
|
+
parsed = JSON.parse(line);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (parsed.event === "ready") {
|
|
536
|
+
const pending = this.pending.get("__ready__");
|
|
537
|
+
if (pending) {
|
|
538
|
+
this.pending.delete("__ready__");
|
|
539
|
+
pending.resolve(true);
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const id = String(parsed.id || "");
|
|
544
|
+
if (!id) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const pending = this.pending.get(id);
|
|
548
|
+
if (!pending) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
this.pending.delete(id);
|
|
552
|
+
if (parsed.ok === true) {
|
|
553
|
+
pending.resolve(parsed.result);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
pending.reject(new Error(String(parsed.error || "Falha no helper do WhatsApp.")));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async waitForReady() {
|
|
561
|
+
await new Promise((resolve, reject) => {
|
|
562
|
+
this.pending.set("__ready__", { resolve: () => resolve(), reject });
|
|
563
|
+
setTimeout(() => {
|
|
564
|
+
if (this.pending.has("__ready__")) {
|
|
565
|
+
this.pending.delete("__ready__");
|
|
566
|
+
reject(new Error("Helper do WhatsApp demorou demais para iniciar."));
|
|
567
|
+
}
|
|
568
|
+
}, 20_000);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
async call(method, params) {
|
|
572
|
+
const child = this.child;
|
|
573
|
+
if (!child) {
|
|
574
|
+
throw new Error("Helper do WhatsApp nao esta em execucao.");
|
|
575
|
+
}
|
|
576
|
+
const id = randomUUID();
|
|
577
|
+
const payload = JSON.stringify({ id, method, params: params || {} }) + "\n";
|
|
578
|
+
const promise = new Promise((resolve, reject) => {
|
|
579
|
+
this.pending.set(id, { resolve, reject });
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
if (this.pending.has(id)) {
|
|
582
|
+
this.pending.delete(id);
|
|
583
|
+
reject(new Error(`Helper do WhatsApp demorou demais para responder ao metodo ${method}.`));
|
|
584
|
+
}
|
|
585
|
+
}, 30_000);
|
|
586
|
+
});
|
|
587
|
+
child.stdin.write(payload, "utf8");
|
|
588
|
+
return await promise;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
export const MACOS_WHATSAPP_HELPER_SWIFT_SOURCE = String.raw `import AppKit
|
|
2
|
+
import Foundation
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
6
|
+
private static let chromeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
|
|
7
|
+
private let app = NSApplication.shared
|
|
8
|
+
private let window: NSWindow
|
|
9
|
+
private let webView: WKWebView
|
|
10
|
+
private var stdinBuffer = Data()
|
|
11
|
+
private let stdoutHandle = FileHandle.standardOutput
|
|
12
|
+
|
|
13
|
+
override init() {
|
|
14
|
+
let config = WKWebViewConfiguration()
|
|
15
|
+
config.websiteDataStore = .default()
|
|
16
|
+
self.webView = WKWebView(frame: .zero, configuration: config)
|
|
17
|
+
self.window = NSWindow(
|
|
18
|
+
contentRect: NSRect(x: -2200, y: 80, width: 1320, height: 920),
|
|
19
|
+
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
20
|
+
backing: .buffered,
|
|
21
|
+
defer: false
|
|
22
|
+
)
|
|
23
|
+
super.init()
|
|
24
|
+
app.setActivationPolicy(.accessory)
|
|
25
|
+
window.title = "Otto WhatsApp Helper"
|
|
26
|
+
window.isReleasedWhenClosed = false
|
|
27
|
+
window.contentView = webView
|
|
28
|
+
webView.navigationDelegate = self
|
|
29
|
+
webView.customUserAgent = Self.chromeUserAgent
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func run() {
|
|
33
|
+
setupStdin()
|
|
34
|
+
send(["event": "ready"])
|
|
35
|
+
DispatchQueue.main.async {
|
|
36
|
+
self.ensureWhatsAppLoaded()
|
|
37
|
+
self.hideBackground()
|
|
38
|
+
}
|
|
39
|
+
app.run()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func setupStdin() {
|
|
43
|
+
let handle = FileHandle.standardInput
|
|
44
|
+
handle.readabilityHandler = { [weak self] readable in
|
|
45
|
+
let data = readable.availableData
|
|
46
|
+
guard let self = self else { return }
|
|
47
|
+
if data.isEmpty {
|
|
48
|
+
DispatchQueue.main.async {
|
|
49
|
+
NSApp.terminate(nil)
|
|
50
|
+
}
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
self.stdinBuffer.append(data)
|
|
54
|
+
while let newlineRange = self.stdinBuffer.firstRange(of: Data([0x0A])) {
|
|
55
|
+
let lineData = self.stdinBuffer.subdata(in: 0..<newlineRange.lowerBound)
|
|
56
|
+
self.stdinBuffer.removeSubrange(0...newlineRange.lowerBound)
|
|
57
|
+
self.handleLine(lineData)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func handleLine(_ lineData: Data) {
|
|
63
|
+
guard !lineData.isEmpty else { return }
|
|
64
|
+
guard
|
|
65
|
+
let object = try? JSONSerialization.jsonObject(with: lineData),
|
|
66
|
+
let request = object as? [String: Any],
|
|
67
|
+
let id = request["id"] as? String,
|
|
68
|
+
let method = request["method"] as? String
|
|
69
|
+
else {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let params = request["params"] as? [String: Any] ?? [:]
|
|
74
|
+
DispatchQueue.main.async {
|
|
75
|
+
self.handleRequest(id: id, method: method, params: params)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func handleRequest(id: String, method: String, params: [String: Any]) {
|
|
80
|
+
switch method {
|
|
81
|
+
case "ping":
|
|
82
|
+
sendResponse(id: id, result: ["ok": true])
|
|
83
|
+
case "show_setup":
|
|
84
|
+
showSetup()
|
|
85
|
+
sendResponse(id: id, result: ["visible": true])
|
|
86
|
+
case "hide_background":
|
|
87
|
+
hideBackground()
|
|
88
|
+
sendResponse(id: id, result: ["background": true])
|
|
89
|
+
case "load_whatsapp":
|
|
90
|
+
ensureWhatsAppLoaded()
|
|
91
|
+
sendResponse(id: id, result: ["url": webView.url?.absoluteString ?? ""])
|
|
92
|
+
case "evaluate_js":
|
|
93
|
+
let script = String(describing: params["script"] ?? "")
|
|
94
|
+
evaluateJavaScript(script) { result, error in
|
|
95
|
+
if let error = error {
|
|
96
|
+
self.sendResponse(id: id, error: error.localizedDescription)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
self.sendResponse(id: id, result: self.makeJSONCompatible(result))
|
|
100
|
+
}
|
|
101
|
+
case "close":
|
|
102
|
+
sendResponse(id: id, result: ["closing": true])
|
|
103
|
+
NSApp.terminate(nil)
|
|
104
|
+
default:
|
|
105
|
+
sendResponse(id: id, error: "Metodo desconhecido: \(method)")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private func ensureWhatsAppLoaded() {
|
|
110
|
+
if let current = webView.url?.absoluteString.lowercased(), current.hasPrefix("https://web.whatsapp.com") {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if let url = URL(string: "https://web.whatsapp.com") {
|
|
114
|
+
webView.load(URLRequest(url: url))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func showSetup() {
|
|
119
|
+
if let screen = NSScreen.main {
|
|
120
|
+
let frame = screen.visibleFrame
|
|
121
|
+
let originX = frame.origin.x + max(0, (frame.width - 1320) / 2)
|
|
122
|
+
let originY = frame.origin.y + max(0, (frame.height - 920) / 2)
|
|
123
|
+
window.setFrame(NSRect(x: originX, y: originY, width: 1320, height: 920), display: true)
|
|
124
|
+
} else {
|
|
125
|
+
window.setFrame(NSRect(x: 120, y: 120, width: 1320, height: 920), display: true)
|
|
126
|
+
}
|
|
127
|
+
window.orderFrontRegardless()
|
|
128
|
+
app.activate(ignoringOtherApps: true)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func hideBackground() {
|
|
132
|
+
window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: true)
|
|
133
|
+
window.orderFrontRegardless()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func evaluateJavaScript(_ script: String, completion: @escaping (Any?, Error?) -> Void) {
|
|
137
|
+
webView.evaluateJavaScript(script) { result, error in
|
|
138
|
+
completion(result, error)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func makeJSONCompatible(_ value: Any?) -> Any {
|
|
143
|
+
guard let value = value else { return NSNull() }
|
|
144
|
+
switch value {
|
|
145
|
+
case is NSString, is NSNumber, is NSNull:
|
|
146
|
+
return value
|
|
147
|
+
case let dict as [String: Any]:
|
|
148
|
+
return dict.mapValues { makeJSONCompatible($0) }
|
|
149
|
+
case let array as [Any]:
|
|
150
|
+
return array.map { makeJSONCompatible($0) }
|
|
151
|
+
default:
|
|
152
|
+
return String(describing: value)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func sendResponse(id: String, result: Any? = nil, error: String? = nil) {
|
|
157
|
+
var payload: [String: Any] = ["id": id, "ok": error == nil]
|
|
158
|
+
if let error = error {
|
|
159
|
+
payload["error"] = error
|
|
160
|
+
} else {
|
|
161
|
+
payload["result"] = makeJSONCompatible(result)
|
|
162
|
+
}
|
|
163
|
+
send(payload)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private func send(_ object: [String: Any]) {
|
|
167
|
+
guard let data = try? JSONSerialization.data(withJSONObject: object) else { return }
|
|
168
|
+
var line = data
|
|
169
|
+
line.append(0x0A)
|
|
170
|
+
try? stdoutHandle.write(contentsOf: line)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let helper = OttoWhatsAppHelper()
|
|
175
|
+
helper.run()
|
|
176
|
+
`;
|
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.7.2";
|
|
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;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { getBridgeHomeDir } from "./config.js";
|
|
7
|
+
import { checkMacOSWhatsAppHelperAvailability, MacOSWhatsAppHelperRuntime, } from "./macos_whatsapp_helper.js";
|
|
8
|
+
import { getConfiguredWhatsAppRuntimeProvider } from "./whatsapp_runtime_provider.js";
|
|
6
9
|
export const WHATSAPP_WEB_URL = "https://web.whatsapp.com";
|
|
7
10
|
const DEFAULT_SETUP_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
11
|
const DEFAULT_SESSION_SETTLE_TIMEOUT_MS = 15_000;
|
|
@@ -25,7 +28,7 @@ function playwrightImportCandidates() {
|
|
|
25
28
|
];
|
|
26
29
|
return Array.from(new Set(candidates));
|
|
27
30
|
}
|
|
28
|
-
async function
|
|
31
|
+
async function loadEmbeddedPlaywrightModule() {
|
|
29
32
|
const errors = [];
|
|
30
33
|
for (const candidate of playwrightImportCandidates()) {
|
|
31
34
|
try {
|
|
@@ -78,12 +81,24 @@ export class WhatsAppBackgroundBrowser {
|
|
|
78
81
|
options;
|
|
79
82
|
context = null;
|
|
80
83
|
page = null;
|
|
84
|
+
helperRuntime = null;
|
|
85
|
+
activeProviderKind = null;
|
|
81
86
|
constructor(options = {}) {
|
|
82
87
|
this.options = options;
|
|
83
88
|
}
|
|
84
89
|
static async checkAvailability() {
|
|
90
|
+
const provider = getConfiguredWhatsAppRuntimeProvider();
|
|
91
|
+
if (provider.kind === "macos-helper") {
|
|
92
|
+
const helperAvailability = await checkMacOSWhatsAppHelperAvailability();
|
|
93
|
+
if (helperAvailability.ok) {
|
|
94
|
+
return helperAvailability;
|
|
95
|
+
}
|
|
96
|
+
if (provider.source === "env") {
|
|
97
|
+
return helperAvailability;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
85
100
|
try {
|
|
86
|
-
await
|
|
101
|
+
await loadEmbeddedPlaywrightModule();
|
|
87
102
|
return { ok: true };
|
|
88
103
|
}
|
|
89
104
|
catch (error) {
|
|
@@ -94,10 +109,39 @@ export class WhatsAppBackgroundBrowser {
|
|
|
94
109
|
}
|
|
95
110
|
}
|
|
96
111
|
async start() {
|
|
97
|
-
if (this.context && this.page) {
|
|
112
|
+
if (this.helperRuntime || (this.context && this.page)) {
|
|
98
113
|
return;
|
|
99
114
|
}
|
|
100
|
-
const
|
|
115
|
+
const provider = getConfiguredWhatsAppRuntimeProvider();
|
|
116
|
+
if (provider.kind === "macos-helper" && process.platform === "darwin") {
|
|
117
|
+
const helperAvailability = await checkMacOSWhatsAppHelperAvailability();
|
|
118
|
+
if (helperAvailability.ok) {
|
|
119
|
+
try {
|
|
120
|
+
this.helperRuntime = new MacOSWhatsAppHelperRuntime({ background: this.options.background });
|
|
121
|
+
await this.helperRuntime.start();
|
|
122
|
+
if (this.options.background) {
|
|
123
|
+
await this.helperRuntime.hideBackground();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await this.helperRuntime.showSetup();
|
|
127
|
+
}
|
|
128
|
+
this.activeProviderKind = "macos-helper";
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.helperRuntime = null;
|
|
133
|
+
if (provider.source === "env") {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
137
|
+
console.warn(`[otto-bridge] whatsapp runtime fallback para embedded-playwright: ${detail}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (provider.source === "env") {
|
|
141
|
+
throw new Error(helperAvailability.reason || "O helper macOS do WhatsApp nao esta disponivel nesta maquina.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const playwright = await loadEmbeddedPlaywrightModule();
|
|
101
145
|
await ensureWhatsAppBrowserUserDataDir();
|
|
102
146
|
this.context = await playwright.chromium.launchPersistentContext(getWhatsAppBrowserUserDataDir(), {
|
|
103
147
|
headless: this.options.headless === true,
|
|
@@ -114,20 +158,31 @@ export class WhatsAppBackgroundBrowser {
|
|
|
114
158
|
const pages = this.context.pages();
|
|
115
159
|
this.page = pages[0] || await this.context.newPage();
|
|
116
160
|
await this.ensureWhatsAppPage();
|
|
161
|
+
this.activeProviderKind = "embedded-playwright";
|
|
117
162
|
if (this.options.background) {
|
|
118
163
|
await this.ensureBackgroundPlacement();
|
|
119
164
|
}
|
|
120
165
|
}
|
|
121
166
|
async close() {
|
|
167
|
+
const helper = this.helperRuntime;
|
|
168
|
+
this.helperRuntime = null;
|
|
122
169
|
const current = this.context;
|
|
123
170
|
this.page = null;
|
|
124
171
|
this.context = null;
|
|
172
|
+
this.activeProviderKind = null;
|
|
173
|
+
if (helper) {
|
|
174
|
+
await helper.close().catch(() => undefined);
|
|
175
|
+
}
|
|
125
176
|
if (!current) {
|
|
126
177
|
return;
|
|
127
178
|
}
|
|
128
179
|
await current.close().catch(() => undefined);
|
|
129
180
|
}
|
|
130
181
|
async waitForTimeout(timeoutMs) {
|
|
182
|
+
if (this.helperRuntime) {
|
|
183
|
+
await this.helperRuntime.waitForTimeout(timeoutMs);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
131
186
|
await this.page?.waitForTimeout(Math.max(0, Number(timeoutMs || 0)));
|
|
132
187
|
}
|
|
133
188
|
async ensureBackgroundPlacement() {
|
|
@@ -228,6 +283,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
228
283
|
return candidates[0]?.pid || null;
|
|
229
284
|
}
|
|
230
285
|
async getSessionState() {
|
|
286
|
+
if (this.helperRuntime) {
|
|
287
|
+
return await this.helperRuntime.getSessionState();
|
|
288
|
+
}
|
|
231
289
|
const state = await this.withPage(async (page) => {
|
|
232
290
|
await this.ensureWhatsAppPage();
|
|
233
291
|
return page.evaluate(() => {
|
|
@@ -272,6 +330,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
272
330
|
};
|
|
273
331
|
}
|
|
274
332
|
async ensureReady() {
|
|
333
|
+
if (this.helperRuntime) {
|
|
334
|
+
return await this.helperRuntime.ensureReady();
|
|
335
|
+
}
|
|
275
336
|
const state = await this.waitForStableSessionState();
|
|
276
337
|
if (state.connected) {
|
|
277
338
|
return state;
|
|
@@ -282,6 +343,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
282
343
|
throw new Error("Nao consegui confirmar uma sessao pronta do WhatsApp Web neste browser em background.");
|
|
283
344
|
}
|
|
284
345
|
async waitForLogin(timeoutMs = DEFAULT_SETUP_TIMEOUT_MS) {
|
|
346
|
+
if (this.helperRuntime) {
|
|
347
|
+
return await this.helperRuntime.waitForLogin(timeoutMs);
|
|
348
|
+
}
|
|
285
349
|
const deadline = Date.now() + Math.max(10_000, timeoutMs);
|
|
286
350
|
let lastState = null;
|
|
287
351
|
while (Date.now() < deadline) {
|
|
@@ -294,6 +358,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
294
358
|
return lastState || await this.getSessionState();
|
|
295
359
|
}
|
|
296
360
|
async waitForStableSessionState(options) {
|
|
361
|
+
if (this.helperRuntime) {
|
|
362
|
+
return await this.helperRuntime.waitForStableSessionState(options);
|
|
363
|
+
}
|
|
297
364
|
const timeoutMs = Math.max(2_000, Number(options?.timeoutMs || DEFAULT_SESSION_SETTLE_TIMEOUT_MS));
|
|
298
365
|
const pollIntervalMs = Math.max(250, Number(options?.pollIntervalMs || DEFAULT_SESSION_POLL_INTERVAL_MS));
|
|
299
366
|
const qrStabilityWindowMs = Math.max(pollIntervalMs, Number(options?.qrStabilityWindowMs || DEFAULT_QR_STABILITY_WINDOW_MS));
|
|
@@ -321,6 +388,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
321
388
|
return lastState || await this.getSessionState();
|
|
322
389
|
}
|
|
323
390
|
async selectConversation(contact) {
|
|
391
|
+
if (this.helperRuntime) {
|
|
392
|
+
return await this.helperRuntime.selectConversation(contact);
|
|
393
|
+
}
|
|
324
394
|
await this.ensureReady();
|
|
325
395
|
const prepared = await this.withPage(async (page) => {
|
|
326
396
|
const focusResult = await page.evaluate(() => {
|
|
@@ -453,32 +523,88 @@ export class WhatsAppBackgroundBrowser {
|
|
|
453
523
|
return false;
|
|
454
524
|
}
|
|
455
525
|
async sendMessage(text) {
|
|
526
|
+
if (this.helperRuntime) {
|
|
527
|
+
await this.helperRuntime.sendMessage(text);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
456
530
|
await this.ensureReady();
|
|
457
531
|
const result = await this.withPage(async (page) => {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
532
|
+
let focusedComposer = { ok: false };
|
|
533
|
+
const deadline = Date.now() + 8_000;
|
|
534
|
+
while (Date.now() < deadline) {
|
|
535
|
+
focusedComposer = await page.evaluate(() => {
|
|
536
|
+
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
|
|
537
|
+
function isVisible(element) {
|
|
538
|
+
if (!(element instanceof HTMLElement))
|
|
539
|
+
return false;
|
|
540
|
+
const rect = element.getBoundingClientRect();
|
|
541
|
+
if (rect.width < 6 || rect.height < 6)
|
|
542
|
+
return false;
|
|
543
|
+
const style = window.getComputedStyle(element);
|
|
544
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0)
|
|
545
|
+
return false;
|
|
546
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
547
|
+
}
|
|
548
|
+
const uniqueTargets = new Set();
|
|
549
|
+
const rawCandidates = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea, div[contenteditable="true"][role="textbox"], div[contenteditable="true"][data-tab], textarea'));
|
|
550
|
+
const candidates = rawCandidates
|
|
551
|
+
.map((node) => {
|
|
552
|
+
if (!(node instanceof HTMLElement))
|
|
553
|
+
return null;
|
|
554
|
+
const resolved = node.matches('[data-testid="conversation-compose-box-input"]')
|
|
555
|
+
? node.querySelector('[contenteditable="true"], textarea')
|
|
556
|
+
: node;
|
|
557
|
+
return resolved instanceof HTMLElement ? resolved : null;
|
|
558
|
+
})
|
|
559
|
+
.filter((node) => {
|
|
560
|
+
if (!(node instanceof HTMLElement) || uniqueTargets.has(node) || !isVisible(node)) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
uniqueTargets.add(node);
|
|
564
|
+
return true;
|
|
565
|
+
})
|
|
566
|
+
.map((node) => {
|
|
567
|
+
const rect = node.getBoundingClientRect();
|
|
568
|
+
const label = normalize(node.getAttribute("aria-label")
|
|
569
|
+
|| node.getAttribute("aria-placeholder")
|
|
570
|
+
|| node.getAttribute("placeholder")
|
|
571
|
+
|| node.getAttribute("data-testid")
|
|
572
|
+
|| node.textContent
|
|
573
|
+
|| "");
|
|
574
|
+
let score = 0;
|
|
575
|
+
if (node.closest("footer"))
|
|
576
|
+
score += 120;
|
|
577
|
+
if (node.closest("main"))
|
|
578
|
+
score += 60;
|
|
579
|
+
if (rect.top > window.innerHeight * 0.55)
|
|
580
|
+
score += 80;
|
|
581
|
+
if (rect.left > window.innerWidth * 0.2)
|
|
582
|
+
score += 30;
|
|
583
|
+
if (label.includes("mensagem") || label.includes("message") || label.includes("digite") || label.includes("type"))
|
|
584
|
+
score += 160;
|
|
585
|
+
if (label.includes("search") || label.includes("pesquisar") || label.includes("procure") || label.includes("chat list"))
|
|
586
|
+
score -= 240;
|
|
587
|
+
if (node.closest('[data-testid="chat-list-search"], [role="search"]'))
|
|
588
|
+
score -= 260;
|
|
589
|
+
if (rect.top < 240)
|
|
590
|
+
score -= 80;
|
|
591
|
+
return { node, score };
|
|
592
|
+
})
|
|
593
|
+
.filter((item) => item.score > 0)
|
|
594
|
+
.sort((left, right) => right.score - left.score || right.node.getBoundingClientRect().top - left.node.getBoundingClientRect().top);
|
|
595
|
+
if (!candidates.length) {
|
|
596
|
+
return { ok: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
|
|
597
|
+
}
|
|
598
|
+
const composer = candidates[0].node;
|
|
599
|
+
composer.focus();
|
|
600
|
+
composer.click();
|
|
601
|
+
return { ok: true };
|
|
602
|
+
});
|
|
603
|
+
if (focusedComposer.ok) {
|
|
604
|
+
break;
|
|
476
605
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
composer.click();
|
|
480
|
-
return { ok: true };
|
|
481
|
-
});
|
|
606
|
+
await page.waitForTimeout(450);
|
|
607
|
+
}
|
|
482
608
|
if (!focusedComposer.ok) {
|
|
483
609
|
return { sent: false, reason: String(focusedComposer.reason || "Nao achei o campo de mensagem do WhatsApp Web.") };
|
|
484
610
|
}
|
|
@@ -527,6 +653,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
527
653
|
}
|
|
528
654
|
}
|
|
529
655
|
async readVisibleConversation(limit) {
|
|
656
|
+
if (this.helperRuntime) {
|
|
657
|
+
return await this.helperRuntime.readVisibleConversation(limit);
|
|
658
|
+
}
|
|
530
659
|
await this.ensureReady();
|
|
531
660
|
const result = await this.withPage((page) => page.evaluate((maxMessages) => {
|
|
532
661
|
function isVisible(element) {
|
|
@@ -569,6 +698,9 @@ export class WhatsAppBackgroundBrowser {
|
|
|
569
698
|
};
|
|
570
699
|
}
|
|
571
700
|
async verifyLastMessage(expectedText) {
|
|
701
|
+
if (this.helperRuntime) {
|
|
702
|
+
return await this.helperRuntime.verifyLastMessage(expectedText);
|
|
703
|
+
}
|
|
572
704
|
const chat = await this.readVisibleConversation(6);
|
|
573
705
|
if (!chat.messages.length) {
|
|
574
706
|
return {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
export const DEFAULT_WHATSAPP_RUNTIME_PROVIDER = process.platform === "darwin" ? "macos-helper" : "embedded-playwright";
|
|
3
|
+
export function resolveWhatsAppRuntimeProviderKind(rawValue) {
|
|
4
|
+
const normalized = String(rawValue || "").trim().toLowerCase();
|
|
5
|
+
if (normalized === "macos-helper" || normalized === "macos_helper") {
|
|
6
|
+
return "macos-helper";
|
|
7
|
+
}
|
|
8
|
+
return DEFAULT_WHATSAPP_RUNTIME_PROVIDER;
|
|
9
|
+
}
|
|
10
|
+
export function getConfiguredWhatsAppRuntimeProvider() {
|
|
11
|
+
const rawEnvValue = process.env.OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER;
|
|
12
|
+
const source = String(rawEnvValue || "").trim() ? "env" : "default";
|
|
13
|
+
const kind = resolveWhatsAppRuntimeProviderKind(rawEnvValue);
|
|
14
|
+
if (kind === "macos-helper") {
|
|
15
|
+
return {
|
|
16
|
+
kind,
|
|
17
|
+
source,
|
|
18
|
+
supportsDocklessBackground: true,
|
|
19
|
+
description: "Provider nativo macOS com helper WKWebView rodando sem aparecer no Dock.",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
kind,
|
|
24
|
+
source,
|
|
25
|
+
supportsDocklessBackground: false,
|
|
26
|
+
description: "Provider embutido com Playwright/Chromium persistente para o WhatsApp Web.",
|
|
27
|
+
};
|
|
28
|
+
}
|