@leg3ndy/otto-bridge 0.5.9 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/executors/native_macos.js +365 -28
- package/dist/extensions.js +79 -0
- package/dist/main.js +252 -5
- package/dist/types.js +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { JobCancelledError } from "./shared.js";
|
|
7
|
+
import { loadManagedBridgeExtensionState, saveManagedBridgeExtensionState, } from "../extensions.js";
|
|
7
8
|
import { postDeviceJson, uploadDeviceJobArtifact } from "../http.js";
|
|
8
9
|
const KNOWN_APPS = [
|
|
9
10
|
{ canonical: "Safari", patterns: [/\bsafari\b/i] },
|
|
@@ -30,6 +31,19 @@ const KNOWN_SITES = [
|
|
|
30
31
|
{ label: "WhatsApp Web", url: "https://web.whatsapp.com", patterns: [/\bwhatsapp\b/i] },
|
|
31
32
|
{ label: "X", url: "https://x.com", patterns: [/\bx\.com\b/i, /\btwitter\b/i, /\bxis\b/i] },
|
|
32
33
|
];
|
|
34
|
+
const WHATSAPP_WEB_EXTENSION_SLUG = "whatsappweb";
|
|
35
|
+
const WHATSAPP_WEB_URL = "https://web.whatsapp.com";
|
|
36
|
+
const FILE_SEARCH_SKIP_DIRS = new Set([
|
|
37
|
+
".git",
|
|
38
|
+
"node_modules",
|
|
39
|
+
".venv",
|
|
40
|
+
".next",
|
|
41
|
+
"dist",
|
|
42
|
+
"build",
|
|
43
|
+
".cache",
|
|
44
|
+
"Library",
|
|
45
|
+
".Trash",
|
|
46
|
+
]);
|
|
33
47
|
const GENERIC_VISUAL_STOP_WORDS = new Set([
|
|
34
48
|
"o",
|
|
35
49
|
"a",
|
|
@@ -449,6 +463,14 @@ function humanizeUrl(url) {
|
|
|
449
463
|
return normalized;
|
|
450
464
|
}
|
|
451
465
|
}
|
|
466
|
+
function urlHostname(url) {
|
|
467
|
+
try {
|
|
468
|
+
return new URL(normalizeUrl(url)).hostname.replace(/^www\./i, "").toLowerCase();
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
452
474
|
function uniqueStrings(values) {
|
|
453
475
|
const seen = new Set();
|
|
454
476
|
const result = [];
|
|
@@ -980,6 +1002,17 @@ export class NativeMacOSJobExecutor {
|
|
|
980
1002
|
if (action.type === "press_shortcut") {
|
|
981
1003
|
await reporter.progress(progressPercent, `Enviando atalho ${action.shortcut}`);
|
|
982
1004
|
await this.pressShortcut(action.shortcut);
|
|
1005
|
+
if (action.shortcut.startsWith("media_")) {
|
|
1006
|
+
const mediaSummaryMap = {
|
|
1007
|
+
media_next: "Acionei o comando de próxima mídia no macOS.",
|
|
1008
|
+
media_previous: "Acionei o comando de mídia anterior no macOS.",
|
|
1009
|
+
media_pause: "Acionei o comando de pausar mídia no macOS.",
|
|
1010
|
+
media_resume: "Acionei o comando de retomar mídia no macOS.",
|
|
1011
|
+
media_play: "Acionei o comando de reproduzir mídia no macOS.",
|
|
1012
|
+
media_play_pause: "Acionei o comando de play/pause de mídia no macOS.",
|
|
1013
|
+
};
|
|
1014
|
+
completionNotes.push(mediaSummaryMap[action.shortcut] || `Acionei ${action.shortcut} no macOS.`);
|
|
1015
|
+
}
|
|
983
1016
|
continue;
|
|
984
1017
|
}
|
|
985
1018
|
if (action.type === "create_note") {
|
|
@@ -1123,17 +1156,13 @@ export class NativeMacOSJobExecutor {
|
|
|
1123
1156
|
}
|
|
1124
1157
|
if (action.type === "whatsapp_send_message") {
|
|
1125
1158
|
await reporter.progress(progressPercent, `Abrindo a conversa do WhatsApp com ${action.contact}`);
|
|
1126
|
-
await this.focusApp("Safari");
|
|
1127
1159
|
await this.ensureWhatsAppWebReady();
|
|
1128
1160
|
const selected = await this.selectWhatsAppConversation(action.contact);
|
|
1129
1161
|
if (!selected) {
|
|
1130
1162
|
throw new Error(`Nao consegui localizar a conversa do WhatsApp com ${action.contact}.`);
|
|
1131
1163
|
}
|
|
1132
|
-
await reporter.progress(progressPercent, `
|
|
1133
|
-
await this.
|
|
1134
|
-
await this.typeText(action.text);
|
|
1135
|
-
await delay(250);
|
|
1136
|
-
await this.pressShortcut("return");
|
|
1164
|
+
await reporter.progress(progressPercent, `Enviando a mensagem para ${action.contact} no WhatsApp`);
|
|
1165
|
+
await this.sendWhatsAppMessage(action.text);
|
|
1137
1166
|
await delay(900);
|
|
1138
1167
|
const verification = await this.verifyWhatsAppLastMessage(action.text);
|
|
1139
1168
|
if (!verification.ok) {
|
|
@@ -1149,7 +1178,6 @@ export class NativeMacOSJobExecutor {
|
|
|
1149
1178
|
}
|
|
1150
1179
|
if (action.type === "whatsapp_read_chat") {
|
|
1151
1180
|
await reporter.progress(progressPercent, `Abrindo a conversa do WhatsApp com ${action.contact}`);
|
|
1152
|
-
await this.focusApp("Safari");
|
|
1153
1181
|
await this.ensureWhatsAppWebReady();
|
|
1154
1182
|
const selected = await this.selectWhatsAppConversation(action.contact);
|
|
1155
1183
|
if (!selected) {
|
|
@@ -1429,6 +1457,15 @@ export class NativeMacOSJobExecutor {
|
|
|
1429
1457
|
await this.focusApp(app);
|
|
1430
1458
|
}
|
|
1431
1459
|
async openUrl(url, app) {
|
|
1460
|
+
if (app === "Safari") {
|
|
1461
|
+
const reused = await this.tryReuseSafariTab(url);
|
|
1462
|
+
if (!reused) {
|
|
1463
|
+
await this.runCommand("open", ["-a", app, url]);
|
|
1464
|
+
}
|
|
1465
|
+
await this.focusApp(app);
|
|
1466
|
+
this.lastActiveApp = app;
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1432
1469
|
if (app) {
|
|
1433
1470
|
await this.runCommand("open", ["-a", app, url]);
|
|
1434
1471
|
await this.focusApp(app);
|
|
@@ -1437,6 +1474,50 @@ export class NativeMacOSJobExecutor {
|
|
|
1437
1474
|
}
|
|
1438
1475
|
await this.runCommand("open", [url]);
|
|
1439
1476
|
}
|
|
1477
|
+
async tryReuseSafariTab(url) {
|
|
1478
|
+
const targetUrl = normalizeUrl(url);
|
|
1479
|
+
const targetHost = urlHostname(targetUrl);
|
|
1480
|
+
if (!targetHost) {
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
const script = `
|
|
1484
|
+
set targetHost to "${escapeAppleScript(targetHost)}"
|
|
1485
|
+
set targetUrl to "${escapeAppleScript(targetUrl)}"
|
|
1486
|
+
tell application "Safari"
|
|
1487
|
+
if (count of windows) = 0 then return "NO_WINDOW"
|
|
1488
|
+
set matchedWindow to missing value
|
|
1489
|
+
set matchedTab to missing value
|
|
1490
|
+
repeat with w in windows
|
|
1491
|
+
repeat with t in tabs of w
|
|
1492
|
+
try
|
|
1493
|
+
set tabUrl to (URL of t) as text
|
|
1494
|
+
on error
|
|
1495
|
+
set tabUrl to ""
|
|
1496
|
+
end try
|
|
1497
|
+
if tabUrl is not "" and tabUrl contains targetHost then
|
|
1498
|
+
set matchedWindow to w
|
|
1499
|
+
set matchedTab to t
|
|
1500
|
+
exit repeat
|
|
1501
|
+
end if
|
|
1502
|
+
end repeat
|
|
1503
|
+
if matchedTab is not missing value then exit repeat
|
|
1504
|
+
end repeat
|
|
1505
|
+
if matchedTab is missing value then return "NO_MATCH"
|
|
1506
|
+
set index of matchedWindow to 1
|
|
1507
|
+
set current tab of matchedWindow to matchedTab
|
|
1508
|
+
set URL of matchedTab to targetUrl
|
|
1509
|
+
activate
|
|
1510
|
+
return "REUSED"
|
|
1511
|
+
end tell
|
|
1512
|
+
`;
|
|
1513
|
+
try {
|
|
1514
|
+
const { stdout } = await this.runCommandCapture("osascript", ["-e", script]);
|
|
1515
|
+
return String(stdout || "").trim() === "REUSED";
|
|
1516
|
+
}
|
|
1517
|
+
catch {
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1440
1521
|
async createNote(text, title) {
|
|
1441
1522
|
const noteTitle = clipText((title || deriveNoteTitle(text)).trim() || "Nota Otto", 120);
|
|
1442
1523
|
const noteBodyText = stripDuplicatedTitleFromText(text, noteTitle);
|
|
@@ -1978,11 +2059,114 @@ end repeat
|
|
|
1978
2059
|
`;
|
|
1979
2060
|
await this.runCommand("osascript", ["-e", script]);
|
|
1980
2061
|
}
|
|
2062
|
+
hasInstalledBridgeExtension(slug) {
|
|
2063
|
+
return Array.isArray(this.bridgeConfig?.installedExtensions)
|
|
2064
|
+
? this.bridgeConfig?.installedExtensions.includes(slug)
|
|
2065
|
+
: false;
|
|
2066
|
+
}
|
|
2067
|
+
getWhatsAppWebScriptOptions(activate = false) {
|
|
2068
|
+
return {
|
|
2069
|
+
targetUrlIncludes: "web.whatsapp.com",
|
|
2070
|
+
activate,
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
async syncWhatsAppExtensionState(status, notes) {
|
|
2074
|
+
const current = await loadManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG).catch(() => null);
|
|
2075
|
+
if (!current) {
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
await saveManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG, {
|
|
2079
|
+
...current,
|
|
2080
|
+
status,
|
|
2081
|
+
notes,
|
|
2082
|
+
lastStatusCheckAt: new Date().toISOString(),
|
|
2083
|
+
}).catch(() => undefined);
|
|
2084
|
+
}
|
|
2085
|
+
async readWhatsAppWebSessionState() {
|
|
2086
|
+
return this.runSafariJsonScript(`
|
|
2087
|
+
function isVisible(element) {
|
|
2088
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
2089
|
+
const rect = element.getBoundingClientRect();
|
|
2090
|
+
if (rect.width < 4 || rect.height < 4) return false;
|
|
2091
|
+
const style = window.getComputedStyle(element);
|
|
2092
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
2093
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
return {
|
|
2097
|
+
title: document.title || "",
|
|
2098
|
+
url: location.href || "",
|
|
2099
|
+
qrVisible: Array.from(document.querySelectorAll('[data-testid="qrcode"], canvas[aria-label*="Scan"], canvas[aria-label*="scan"], div[data-ref] canvas'))
|
|
2100
|
+
.some((node) => node instanceof HTMLElement ? isVisible(node) : true),
|
|
2101
|
+
paneVisible: Array.from(document.querySelectorAll('#pane-side, [data-testid="chat-list"]'))
|
|
2102
|
+
.some((node) => node instanceof HTMLElement && isVisible(node)),
|
|
2103
|
+
searchVisible: Array.from(document.querySelectorAll('[data-testid="chat-list-search"] [contenteditable="true"], div[contenteditable="true"][role="textbox"]'))
|
|
2104
|
+
.some((node) => node instanceof HTMLElement && isVisible(node)),
|
|
2105
|
+
composerVisible: Array.from(document.querySelectorAll('footer [contenteditable="true"], [data-testid="conversation-compose-box-input"]'))
|
|
2106
|
+
.some((node) => node instanceof HTMLElement && isVisible(node)),
|
|
2107
|
+
};
|
|
2108
|
+
`, {}, this.getWhatsAppWebScriptOptions(false)).then((result) => ({
|
|
2109
|
+
title: asString(result.title) || "",
|
|
2110
|
+
url: asString(result.url) || "",
|
|
2111
|
+
qrVisible: result.qrVisible === true,
|
|
2112
|
+
paneVisible: result.paneVisible === true,
|
|
2113
|
+
searchVisible: result.searchVisible === true,
|
|
2114
|
+
composerVisible: result.composerVisible === true,
|
|
2115
|
+
}));
|
|
2116
|
+
}
|
|
1981
2117
|
async ensureWhatsAppWebReady() {
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2118
|
+
if (!this.hasInstalledBridgeExtension(WHATSAPP_WEB_EXTENSION_SLUG)) {
|
|
2119
|
+
throw new Error("WhatsApp Web nao esta instalado neste Otto Bridge. Rode `otto-bridge extensions --install whatsappweb` e depois `otto-bridge extensions --setup whatsappweb`.");
|
|
2120
|
+
}
|
|
2121
|
+
const currentState = await loadManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG).catch(() => null);
|
|
2122
|
+
if (!currentState || currentState.status === "installed_needs_setup") {
|
|
2123
|
+
throw new Error("WhatsApp Web ainda nao foi configurado neste Otto Bridge. Rode `otto-bridge extensions --setup whatsappweb` para abrir o QR code.");
|
|
2124
|
+
}
|
|
2125
|
+
let sessionState = null;
|
|
2126
|
+
try {
|
|
2127
|
+
sessionState = await this.readWhatsAppWebSessionState();
|
|
2128
|
+
}
|
|
2129
|
+
catch (error) {
|
|
2130
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2131
|
+
if (detail.toLowerCase().includes("aba correspondente")) {
|
|
2132
|
+
await this.openUrl(WHATSAPP_WEB_URL, "Safari");
|
|
2133
|
+
await delay(1400);
|
|
2134
|
+
try {
|
|
2135
|
+
sessionState = await this.readWhatsAppWebSessionState();
|
|
2136
|
+
}
|
|
2137
|
+
catch {
|
|
2138
|
+
sessionState = null;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
else {
|
|
2142
|
+
throw error;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (!sessionState) {
|
|
2146
|
+
const disconnectedStatus = currentState.status === "connected" || currentState.status === "session_expired"
|
|
2147
|
+
? "session_expired"
|
|
2148
|
+
: "not_open";
|
|
2149
|
+
await this.syncWhatsAppExtensionState(disconnectedStatus, disconnectedStatus === "session_expired"
|
|
2150
|
+
? "A sessao do WhatsApp Web expirou ou foi fechada. Rode `otto-bridge extensions --setup whatsappweb` para abrir o login novamente."
|
|
2151
|
+
: "Nao consegui localizar uma aba do WhatsApp Web no Safari.");
|
|
2152
|
+
throw new Error("Nao consegui localizar uma aba do WhatsApp Web no Safari. Rode `otto-bridge extensions --setup whatsappweb` se precisar reabrir a sessao.");
|
|
2153
|
+
}
|
|
2154
|
+
const loggedIn = sessionState.paneVisible || sessionState.searchVisible || sessionState.composerVisible;
|
|
2155
|
+
if (!loggedIn) {
|
|
2156
|
+
const disconnectedStatus = currentState.status === "connected" || currentState.status === "session_expired"
|
|
2157
|
+
? "session_expired"
|
|
2158
|
+
: "waiting_login";
|
|
2159
|
+
await this.syncWhatsAppExtensionState(disconnectedStatus, disconnectedStatus === "session_expired"
|
|
2160
|
+
? "A sessao do WhatsApp Web expirou. Rode `otto-bridge extensions --setup whatsappweb` para abrir o QR code novamente."
|
|
2161
|
+
: sessionState.qrVisible
|
|
2162
|
+
? "QR code visivel. Escaneie com o celular para concluir o login."
|
|
2163
|
+
: "Sessao do WhatsApp Web aberta, mas ainda sem chat disponivel.");
|
|
2164
|
+
if (disconnectedStatus === "session_expired") {
|
|
2165
|
+
throw new Error("A sessao do WhatsApp Web expirou nesta maquina. Rode `otto-bridge extensions --setup whatsappweb` para fazer login de novo e depois `otto-bridge extensions --status whatsappweb`.");
|
|
2166
|
+
}
|
|
2167
|
+
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`.");
|
|
1985
2168
|
}
|
|
2169
|
+
await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web pronta para uso.");
|
|
1986
2170
|
}
|
|
1987
2171
|
async selectWhatsAppConversation(contact) {
|
|
1988
2172
|
const prepared = await this.runSafariJsonScript(`
|
|
@@ -2034,7 +2218,7 @@ if (!candidates.length) {
|
|
|
2034
2218
|
|
|
2035
2219
|
focusAndReplaceContent(candidates[0].element, query);
|
|
2036
2220
|
return { ok: true };
|
|
2037
|
-
`, { contact });
|
|
2221
|
+
`, { contact }, this.getWhatsAppWebScriptOptions(false));
|
|
2038
2222
|
if (!prepared?.ok) {
|
|
2039
2223
|
return false;
|
|
2040
2224
|
}
|
|
@@ -2083,11 +2267,12 @@ if (typeof target.click === "function") {
|
|
|
2083
2267
|
target.click();
|
|
2084
2268
|
}
|
|
2085
2269
|
return { clicked: true };
|
|
2086
|
-
`, { contact });
|
|
2270
|
+
`, { contact }, this.getWhatsAppWebScriptOptions(false));
|
|
2087
2271
|
return Boolean(result?.clicked);
|
|
2088
2272
|
}
|
|
2089
|
-
async
|
|
2273
|
+
async sendWhatsAppMessage(text) {
|
|
2090
2274
|
const result = await this.runSafariJsonScript(`
|
|
2275
|
+
const value = String(__input?.text || "");
|
|
2091
2276
|
function isVisible(element) {
|
|
2092
2277
|
if (!(element instanceof HTMLElement)) return false;
|
|
2093
2278
|
const rect = element.getBoundingClientRect();
|
|
@@ -2097,22 +2282,66 @@ function isVisible(element) {
|
|
|
2097
2282
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
2098
2283
|
}
|
|
2099
2284
|
|
|
2100
|
-
|
|
2285
|
+
function clearAndFillComposer(element, nextValue) {
|
|
2286
|
+
element.focus();
|
|
2287
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
2288
|
+
element.value = "";
|
|
2289
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
|
|
2290
|
+
element.value = nextValue;
|
|
2291
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: nextValue }));
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
const selection = window.getSelection();
|
|
2295
|
+
const range = document.createRange();
|
|
2296
|
+
range.selectNodeContents(element);
|
|
2297
|
+
selection?.removeAllRanges();
|
|
2298
|
+
selection?.addRange(range);
|
|
2299
|
+
document.execCommand("selectAll", false);
|
|
2300
|
+
document.execCommand("delete", false);
|
|
2301
|
+
document.execCommand("insertText", false, nextValue);
|
|
2302
|
+
if ((element.innerText || "").trim() !== nextValue.trim()) {
|
|
2303
|
+
element.textContent = nextValue;
|
|
2304
|
+
}
|
|
2305
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: nextValue }));
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const candidates = Array.from(document.querySelectorAll('footer div[contenteditable="true"], [data-testid="conversation-compose-box-input"], main footer [contenteditable="true"], footer textarea'))
|
|
2101
2309
|
.filter((node) => node instanceof HTMLElement)
|
|
2102
2310
|
.filter((node) => isVisible(node))
|
|
2103
2311
|
.sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
|
|
2104
2312
|
|
|
2105
2313
|
if (!candidates.length) {
|
|
2106
|
-
return {
|
|
2314
|
+
return { sent: false, reason: "Nao achei o campo de mensagem do WhatsApp Web." };
|
|
2107
2315
|
}
|
|
2108
2316
|
|
|
2109
2317
|
const composer = candidates[0];
|
|
2110
|
-
composer
|
|
2318
|
+
clearAndFillComposer(composer, value);
|
|
2111
2319
|
composer.click();
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2320
|
+
|
|
2321
|
+
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"]'))
|
|
2322
|
+
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
2323
|
+
.filter((node) => node instanceof HTMLElement)
|
|
2324
|
+
.filter((node) => isVisible(node));
|
|
2325
|
+
|
|
2326
|
+
const sendButton = sendCandidates[0];
|
|
2327
|
+
if (sendButton instanceof HTMLElement) {
|
|
2328
|
+
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
2329
|
+
sendButton.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
2330
|
+
sendButton.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
2331
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2332
|
+
if (typeof sendButton.click === "function") {
|
|
2333
|
+
sendButton.click();
|
|
2334
|
+
}
|
|
2335
|
+
return { sent: true };
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
2339
|
+
composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
2340
|
+
composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
2341
|
+
return { sent: true };
|
|
2342
|
+
`, { text }, this.getWhatsAppWebScriptOptions(false));
|
|
2343
|
+
if (!result?.sent) {
|
|
2344
|
+
throw new Error(result?.reason || "Nao consegui enviar a mensagem no WhatsApp Web.");
|
|
2116
2345
|
}
|
|
2117
2346
|
}
|
|
2118
2347
|
async readWhatsAppVisibleConversation(contact, limit) {
|
|
@@ -2142,7 +2371,7 @@ const messages = containers.map((node) => {
|
|
|
2142
2371
|
}).filter((item) => item.text);
|
|
2143
2372
|
|
|
2144
2373
|
return { messages: messages.slice(-maxMessages) };
|
|
2145
|
-
`, { contact, limit });
|
|
2374
|
+
`, { contact, limit }, this.getWhatsAppWebScriptOptions(false));
|
|
2146
2375
|
const messages = Array.isArray(result?.messages)
|
|
2147
2376
|
? result.messages
|
|
2148
2377
|
.map((item) => ({
|
|
@@ -2256,7 +2485,7 @@ return { messages: messages.slice(-maxMessages) };
|
|
|
2256
2485
|
reason: verificationAnswer,
|
|
2257
2486
|
};
|
|
2258
2487
|
}
|
|
2259
|
-
async runSafariJsonScript(scriptBody, input) {
|
|
2488
|
+
async runSafariJsonScript(scriptBody, input, options) {
|
|
2260
2489
|
const wrappedScript = `
|
|
2261
2490
|
(function(){
|
|
2262
2491
|
const __input = ${JSON.stringify(input || null)};
|
|
@@ -2273,12 +2502,42 @@ ${scriptBody}
|
|
|
2273
2502
|
}
|
|
2274
2503
|
})()
|
|
2275
2504
|
`;
|
|
2505
|
+
const targetUrlIncludes = String(options?.targetUrlIncludes || "").trim();
|
|
2506
|
+
const shouldActivate = options?.activate !== false;
|
|
2276
2507
|
const script = `
|
|
2508
|
+
set targetUrlIncludes to "${escapeAppleScript(targetUrlIncludes)}"
|
|
2509
|
+
set shouldActivate to ${shouldActivate ? "true" : "false"}
|
|
2277
2510
|
tell application "Safari"
|
|
2278
|
-
activate
|
|
2279
2511
|
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
2512
|
+
if shouldActivate then activate
|
|
2513
|
+
set targetWindow to missing value
|
|
2514
|
+
set targetTab to missing value
|
|
2515
|
+
if targetUrlIncludes is not "" then
|
|
2516
|
+
repeat with safariWindow in windows
|
|
2517
|
+
repeat with safariTab in tabs of safariWindow
|
|
2518
|
+
set tabUrl to ""
|
|
2519
|
+
try
|
|
2520
|
+
set tabUrl to URL of safariTab
|
|
2521
|
+
end try
|
|
2522
|
+
if tabUrl contains targetUrlIncludes then
|
|
2523
|
+
set targetWindow to safariWindow
|
|
2524
|
+
set targetTab to safariTab
|
|
2525
|
+
exit repeat
|
|
2526
|
+
end if
|
|
2527
|
+
end repeat
|
|
2528
|
+
if targetTab is not missing value then exit repeat
|
|
2529
|
+
end repeat
|
|
2530
|
+
if targetTab is missing value then error "Safari nao possui aba correspondente a " & targetUrlIncludes
|
|
2531
|
+
else
|
|
2532
|
+
set targetWindow to front window
|
|
2533
|
+
set targetTab to current tab of front window
|
|
2534
|
+
end if
|
|
2535
|
+
if shouldActivate and targetWindow is not missing value then
|
|
2536
|
+
set current tab of targetWindow to targetTab
|
|
2537
|
+
set index of targetWindow to 1
|
|
2538
|
+
end if
|
|
2280
2539
|
delay 0.2
|
|
2281
|
-
set scriptResult to do JavaScript "${escapeAppleScript(wrappedScript)}" in
|
|
2540
|
+
set scriptResult to do JavaScript "${escapeAppleScript(wrappedScript)}" in targetTab
|
|
2282
2541
|
end tell
|
|
2283
2542
|
return scriptResult
|
|
2284
2543
|
`;
|
|
@@ -2673,7 +2932,8 @@ tell application "Safari"
|
|
|
2673
2932
|
activate
|
|
2674
2933
|
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
2675
2934
|
delay 1
|
|
2676
|
-
set
|
|
2935
|
+
set jsCode to "(function(){const title=document.title||'';const url=location.href||'';const text=((document.body&&document.body.innerText)||'').trim().slice(0,12000);const playerButton=document.querySelector('ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button');const playerTitle=(Array.from(document.querySelectorAll('ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=title]')).map((node)=>((node&&node.textContent)||'').trim()).find(Boolean))||'';const playerState=(playerButton&&((playerButton.getAttribute('title')||playerButton.getAttribute('aria-label')||playerButton.textContent)||'').trim())||'';return JSON.stringify({title,url,text,playerTitle,playerState});})();"
|
|
2936
|
+
set pageJson to do JavaScript jsCode in current tab of front window
|
|
2677
2937
|
end tell
|
|
2678
2938
|
return pageJson
|
|
2679
2939
|
`;
|
|
@@ -3055,8 +3315,8 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
3055
3315
|
resized,
|
|
3056
3316
|
};
|
|
3057
3317
|
}
|
|
3058
|
-
async readLocalFile(filePath, maxChars =
|
|
3059
|
-
const resolved =
|
|
3318
|
+
async readLocalFile(filePath, maxChars = 4000) {
|
|
3319
|
+
const resolved = await this.resolveReadableFilePath(filePath);
|
|
3060
3320
|
const extension = path.extname(resolved).toLowerCase();
|
|
3061
3321
|
if (TEXTUTIL_READABLE_EXTENSIONS.has(extension)) {
|
|
3062
3322
|
const { stdout } = await this.runCommandCapture("textutil", [
|
|
@@ -3077,6 +3337,75 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
3077
3337
|
const content = sanitizeTextForJsonTransport(raw.toString("utf8"));
|
|
3078
3338
|
return clipTextPreview(content || "(arquivo vazio)", maxChars);
|
|
3079
3339
|
}
|
|
3340
|
+
async resolveReadableFilePath(filePath) {
|
|
3341
|
+
const resolved = expandUserPath(filePath);
|
|
3342
|
+
try {
|
|
3343
|
+
await stat(resolved);
|
|
3344
|
+
return resolved;
|
|
3345
|
+
}
|
|
3346
|
+
catch {
|
|
3347
|
+
// Continue into heuristic search below.
|
|
3348
|
+
}
|
|
3349
|
+
const filename = path.basename(resolved).trim();
|
|
3350
|
+
if (!filename || filename === "." || filename === path.sep) {
|
|
3351
|
+
return resolved;
|
|
3352
|
+
}
|
|
3353
|
+
const homeDir = os.homedir();
|
|
3354
|
+
const requestedDir = path.dirname(resolved);
|
|
3355
|
+
const preferredRoots = uniqueStrings([
|
|
3356
|
+
requestedDir && requestedDir !== homeDir ? requestedDir : null,
|
|
3357
|
+
path.join(homeDir, "Downloads"),
|
|
3358
|
+
path.join(homeDir, "Desktop"),
|
|
3359
|
+
path.join(homeDir, "Documents"),
|
|
3360
|
+
homeDir,
|
|
3361
|
+
]);
|
|
3362
|
+
const found = await this.findFileByName(filename, preferredRoots);
|
|
3363
|
+
return found || resolved;
|
|
3364
|
+
}
|
|
3365
|
+
async findFileByName(filename, roots) {
|
|
3366
|
+
const target = filename.toLowerCase();
|
|
3367
|
+
for (const root of roots) {
|
|
3368
|
+
let rootStat;
|
|
3369
|
+
try {
|
|
3370
|
+
rootStat = await stat(root);
|
|
3371
|
+
}
|
|
3372
|
+
catch {
|
|
3373
|
+
continue;
|
|
3374
|
+
}
|
|
3375
|
+
if (!rootStat.isDirectory()) {
|
|
3376
|
+
continue;
|
|
3377
|
+
}
|
|
3378
|
+
const queue = [root];
|
|
3379
|
+
while (queue.length > 0) {
|
|
3380
|
+
const current = queue.shift();
|
|
3381
|
+
if (!current)
|
|
3382
|
+
continue;
|
|
3383
|
+
let entries;
|
|
3384
|
+
try {
|
|
3385
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
3386
|
+
}
|
|
3387
|
+
catch {
|
|
3388
|
+
continue;
|
|
3389
|
+
}
|
|
3390
|
+
for (const entry of entries) {
|
|
3391
|
+
const entryPath = path.join(current, entry.name);
|
|
3392
|
+
if (entry.isDirectory()) {
|
|
3393
|
+
if (!FILE_SEARCH_SKIP_DIRS.has(entry.name)) {
|
|
3394
|
+
queue.push(entryPath);
|
|
3395
|
+
}
|
|
3396
|
+
continue;
|
|
3397
|
+
}
|
|
3398
|
+
if (!entry.isFile()) {
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
if (entry.name.toLowerCase() === target) {
|
|
3402
|
+
return entryPath;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3080
3409
|
async listLocalFiles(directoryPath, limit = 40) {
|
|
3081
3410
|
const resolved = expandUserPath(directoryPath);
|
|
3082
3411
|
const entries = await readdir(resolved, { withFileTypes: true });
|
|
@@ -3174,7 +3503,15 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
3174
3503
|
return `${action.app} ficou em foco no macOS`;
|
|
3175
3504
|
}
|
|
3176
3505
|
if (action.type === "press_shortcut") {
|
|
3177
|
-
|
|
3506
|
+
const mediaSummaryMap = {
|
|
3507
|
+
media_next: "Comando de próxima mídia executado no macOS",
|
|
3508
|
+
media_previous: "Comando de mídia anterior executado no macOS",
|
|
3509
|
+
media_pause: "Comando de pausar mídia executado no macOS",
|
|
3510
|
+
media_resume: "Comando de retomar mídia executado no macOS",
|
|
3511
|
+
media_play: "Comando de reproduzir mídia executado no macOS",
|
|
3512
|
+
media_play_pause: "Comando de play/pause de mídia executado no macOS",
|
|
3513
|
+
};
|
|
3514
|
+
return mediaSummaryMap[action.shortcut] || `Atalho ${action.shortcut} executado no macOS`;
|
|
3178
3515
|
}
|
|
3179
3516
|
if (action.type === "create_note") {
|
|
3180
3517
|
return `Nota criada no Notes: ${action.title || deriveNoteTitle(action.text)}`;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getBridgeHomeDir } from "./config.js";
|
|
4
|
+
export const MANAGED_BRIDGE_EXTENSIONS = {
|
|
5
|
+
whatsappweb: {
|
|
6
|
+
displayName: "WhatsApp Web",
|
|
7
|
+
setupUrl: "https://web.whatsapp.com",
|
|
8
|
+
sessionMode: "safari_managed_tab",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export function isManagedBridgeExtensionSlug(value) {
|
|
12
|
+
return Object.prototype.hasOwnProperty.call(MANAGED_BRIDGE_EXTENSIONS, value);
|
|
13
|
+
}
|
|
14
|
+
export function getManagedBridgeExtensionDefinition(slug) {
|
|
15
|
+
return MANAGED_BRIDGE_EXTENSIONS[slug];
|
|
16
|
+
}
|
|
17
|
+
export function getBridgeExtensionsDir() {
|
|
18
|
+
return path.join(getBridgeHomeDir(), "extensions");
|
|
19
|
+
}
|
|
20
|
+
function getManagedExtensionStatePath(slug) {
|
|
21
|
+
return path.join(getBridgeExtensionsDir(), `${slug}.json`);
|
|
22
|
+
}
|
|
23
|
+
export async function ensureBridgeExtensionsDir() {
|
|
24
|
+
await mkdir(getBridgeExtensionsDir(), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
export async function loadManagedBridgeExtensionState(slug) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(getManagedExtensionStatePath(slug), "utf8");
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!parsed || typeof parsed !== "object") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (parsed.slug !== slug) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function saveManagedBridgeExtensionState(slug, state) {
|
|
43
|
+
await ensureBridgeExtensionsDir();
|
|
44
|
+
await writeFile(getManagedExtensionStatePath(slug), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
export async function removeManagedBridgeExtensionState(slug) {
|
|
47
|
+
await rm(getManagedExtensionStatePath(slug), { force: true });
|
|
48
|
+
}
|
|
49
|
+
export async function buildInstalledManagedExtensionState(slug) {
|
|
50
|
+
const definition = getManagedBridgeExtensionDefinition(slug);
|
|
51
|
+
const existing = await loadManagedBridgeExtensionState(slug);
|
|
52
|
+
return {
|
|
53
|
+
slug,
|
|
54
|
+
displayName: definition.displayName,
|
|
55
|
+
setupUrl: definition.setupUrl,
|
|
56
|
+
sessionMode: definition.sessionMode,
|
|
57
|
+
status: existing?.status || "installed_needs_setup",
|
|
58
|
+
installedAt: existing?.installedAt || new Date().toISOString(),
|
|
59
|
+
lastSetupAt: existing?.lastSetupAt,
|
|
60
|
+
lastStatusCheckAt: existing?.lastStatusCheckAt,
|
|
61
|
+
notes: existing?.notes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function formatManagedBridgeExtensionStatus(status) {
|
|
65
|
+
switch (status) {
|
|
66
|
+
case "installed_needs_setup":
|
|
67
|
+
return "instalada, falta setup";
|
|
68
|
+
case "waiting_login":
|
|
69
|
+
return "aguardando login";
|
|
70
|
+
case "connected":
|
|
71
|
+
return "conectada";
|
|
72
|
+
case "session_expired":
|
|
73
|
+
return "sessao expirada";
|
|
74
|
+
case "not_open":
|
|
75
|
+
return "instalada, aba nao aberta";
|
|
76
|
+
default:
|
|
77
|
+
return status;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { clearBridgeConfig, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, saveBridgeConfig, } from "./config.js";
|
|
5
|
+
import { buildInstalledManagedExtensionState, formatManagedBridgeExtensionStatus, getManagedBridgeExtensionDefinition, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, removeManagedBridgeExtensionState, saveManagedBridgeExtensionState, } from "./extensions.js";
|
|
5
6
|
import { pairDevice } from "./pairing.js";
|
|
6
7
|
import { BridgeRuntime } from "./runtime.js";
|
|
7
8
|
import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
|
|
@@ -58,6 +59,8 @@ function printUsage() {
|
|
|
58
59
|
otto-bridge status
|
|
59
60
|
otto-bridge extensions --list
|
|
60
61
|
otto-bridge extensions --install github
|
|
62
|
+
otto-bridge extensions --setup whatsappweb
|
|
63
|
+
otto-bridge extensions --status whatsappweb
|
|
61
64
|
otto-bridge extensions --uninstall github
|
|
62
65
|
otto-bridge version
|
|
63
66
|
otto-bridge update [--tag latest|next] [--dry-run]
|
|
@@ -66,7 +69,9 @@ function printUsage() {
|
|
|
66
69
|
Examples:
|
|
67
70
|
otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
|
|
68
71
|
otto-bridge run
|
|
69
|
-
otto-bridge extensions --install
|
|
72
|
+
otto-bridge extensions --install whatsappweb
|
|
73
|
+
otto-bridge extensions --setup whatsappweb
|
|
74
|
+
otto-bridge extensions --status whatsappweb
|
|
70
75
|
otto-bridge extensions --list
|
|
71
76
|
otto-bridge version
|
|
72
77
|
otto-bridge update
|
|
@@ -94,6 +99,162 @@ function runChildCommand(command, args) {
|
|
|
94
99
|
});
|
|
95
100
|
});
|
|
96
101
|
}
|
|
102
|
+
function runChildCommandCapture(command, args, options) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const child = spawn(command, args, {
|
|
105
|
+
stdio: "pipe",
|
|
106
|
+
env: process.env,
|
|
107
|
+
});
|
|
108
|
+
let stdout = "";
|
|
109
|
+
let stderr = "";
|
|
110
|
+
child.stdout?.setEncoding("utf8");
|
|
111
|
+
child.stderr?.setEncoding("utf8");
|
|
112
|
+
child.stdout?.on("data", (chunk) => {
|
|
113
|
+
stdout += String(chunk);
|
|
114
|
+
});
|
|
115
|
+
child.stderr?.on("data", (chunk) => {
|
|
116
|
+
stderr += String(chunk);
|
|
117
|
+
});
|
|
118
|
+
child.on("error", (error) => {
|
|
119
|
+
reject(error);
|
|
120
|
+
});
|
|
121
|
+
child.on("exit", (code) => {
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
resolve({ stdout, stderr });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const detail = stderr.trim() || stdout.trim();
|
|
127
|
+
reject(new Error(detail || `${command} exited with code ${code ?? "unknown"}`));
|
|
128
|
+
});
|
|
129
|
+
if (options?.stdin !== undefined) {
|
|
130
|
+
child.stdin?.write(options.stdin);
|
|
131
|
+
}
|
|
132
|
+
child.stdin?.end();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function escapeAppleScript(value) {
|
|
136
|
+
return value
|
|
137
|
+
.replace(/\\/g, "\\\\")
|
|
138
|
+
.replace(/"/g, '\\"')
|
|
139
|
+
.replace(/\r/g, "\\r")
|
|
140
|
+
.replace(/\n/g, "\\n");
|
|
141
|
+
}
|
|
142
|
+
async function openManagedExtensionSetup(slug) {
|
|
143
|
+
const definition = getManagedBridgeExtensionDefinition(slug);
|
|
144
|
+
if (process.platform !== "darwin") {
|
|
145
|
+
throw new Error(`${definition.displayName} setup automatico esta disponivel apenas no macOS no momento.`);
|
|
146
|
+
}
|
|
147
|
+
await runChildCommand("open", ["-a", "Safari", definition.setupUrl]);
|
|
148
|
+
}
|
|
149
|
+
async function detectManagedWhatsAppWebStatus() {
|
|
150
|
+
if (process.platform !== "darwin") {
|
|
151
|
+
return {
|
|
152
|
+
status: "installed_needs_setup",
|
|
153
|
+
notes: "Status automatico do WhatsApp Web ainda esta disponivel apenas no macOS.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const jsCode = `
|
|
157
|
+
(function(){
|
|
158
|
+
const qrVisible = Boolean(
|
|
159
|
+
document.querySelector('[data-testid="qrcode"], canvas[aria-label*="Scan"], canvas[aria-label*="scan"], div[data-ref] canvas')
|
|
160
|
+
);
|
|
161
|
+
const paneVisible = Boolean(document.querySelector('#pane-side, [data-testid="chat-list"]'));
|
|
162
|
+
const searchVisible = Boolean(document.querySelector('[data-testid="chat-list-search"] [contenteditable="true"], div[contenteditable="true"][role="textbox"]'));
|
|
163
|
+
const composerVisible = Boolean(document.querySelector('footer [contenteditable="true"], [data-testid="conversation-compose-box-input"]'));
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
ok: true,
|
|
166
|
+
title: document.title || '',
|
|
167
|
+
href: location.href || '',
|
|
168
|
+
qrVisible,
|
|
169
|
+
paneVisible,
|
|
170
|
+
searchVisible,
|
|
171
|
+
composerVisible
|
|
172
|
+
});
|
|
173
|
+
})()
|
|
174
|
+
`;
|
|
175
|
+
const script = `
|
|
176
|
+
set targetUrlIncludes to "web.whatsapp.com"
|
|
177
|
+
tell application "Safari"
|
|
178
|
+
if (count of windows) = 0 then return "{\\"ok\\":false,\\"reason\\":\\"not_open\\"}"
|
|
179
|
+
set targetTab to missing value
|
|
180
|
+
repeat with safariWindow in windows
|
|
181
|
+
repeat with safariTab in tabs of safariWindow
|
|
182
|
+
set tabUrl to ""
|
|
183
|
+
try
|
|
184
|
+
set tabUrl to URL of safariTab
|
|
185
|
+
end try
|
|
186
|
+
if tabUrl contains targetUrlIncludes then
|
|
187
|
+
set targetTab to safariTab
|
|
188
|
+
exit repeat
|
|
189
|
+
end if
|
|
190
|
+
end repeat
|
|
191
|
+
if targetTab is not missing value then exit repeat
|
|
192
|
+
end repeat
|
|
193
|
+
if targetTab is missing value then return "{\\"ok\\":false,\\"reason\\":\\"not_open\\"}"
|
|
194
|
+
set scriptResult to do JavaScript "${escapeAppleScript(jsCode)}" in targetTab
|
|
195
|
+
end tell
|
|
196
|
+
return scriptResult
|
|
197
|
+
`;
|
|
198
|
+
try {
|
|
199
|
+
const { stdout } = await runChildCommandCapture("osascript", ["-e", script]);
|
|
200
|
+
const parsed = JSON.parse(stdout.trim() || "{}");
|
|
201
|
+
if (parsed.ok !== true) {
|
|
202
|
+
return {
|
|
203
|
+
status: "not_open",
|
|
204
|
+
notes: "Nenhuma aba do WhatsApp Web foi encontrada no Safari.",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const qrVisible = parsed.qrVisible === true;
|
|
208
|
+
const paneVisible = parsed.paneVisible === true;
|
|
209
|
+
const searchVisible = parsed.searchVisible === true;
|
|
210
|
+
const composerVisible = parsed.composerVisible === true;
|
|
211
|
+
const title = typeof parsed.title === "string" ? parsed.title : "";
|
|
212
|
+
if (paneVisible || searchVisible || composerVisible) {
|
|
213
|
+
return {
|
|
214
|
+
status: "connected",
|
|
215
|
+
notes: title ? `Sessao conectada em "${title}".` : "Sessao conectada.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (qrVisible) {
|
|
219
|
+
return {
|
|
220
|
+
status: "waiting_login",
|
|
221
|
+
notes: "QR code visivel. Escaneie com o celular para concluir o login.",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
status: "waiting_login",
|
|
226
|
+
notes: title ? `Aba aberta em "${title}", mas o login ainda nao foi confirmado.` : "Aba aberta, mas o login ainda nao foi confirmado.",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
231
|
+
if (detail.toLowerCase().includes("allow javascript from apple events")) {
|
|
232
|
+
return {
|
|
233
|
+
status: "waiting_login",
|
|
234
|
+
notes: "Ative 'Allow JavaScript from Apple Events' no Safari para o bridge verificar o status automaticamente.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function detectManagedExtensionStatus(slug, currentState) {
|
|
241
|
+
if (slug === "whatsappweb") {
|
|
242
|
+
const detected = await detectManagedWhatsAppWebStatus();
|
|
243
|
+
const shouldMarkExpired = (detected.status === "waiting_login"
|
|
244
|
+
&& (currentState?.status === "connected" || currentState?.status === "session_expired"));
|
|
245
|
+
if (!shouldMarkExpired) {
|
|
246
|
+
return detected;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
status: "session_expired",
|
|
250
|
+
notes: "A sessao do WhatsApp Web expirou. Rode `otto-bridge extensions --setup whatsappweb` para abrir o QR code novamente.",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
status: "installed_needs_setup",
|
|
255
|
+
notes: "Esta extensao gerenciada ainda nao possui verificador automatico de status.",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
97
258
|
async function runPairCommand(args) {
|
|
98
259
|
const code = option(args, "code");
|
|
99
260
|
if (!code) {
|
|
@@ -153,14 +314,25 @@ async function runStatusCommand() {
|
|
|
153
314
|
async function runExtensionsCommand(args) {
|
|
154
315
|
const config = await loadRequiredBridgeConfig();
|
|
155
316
|
const installValue = option(args, "install");
|
|
317
|
+
const setupValue = option(args, "setup");
|
|
318
|
+
const statusValue = option(args, "status");
|
|
156
319
|
const uninstallValue = option(args, "uninstall");
|
|
157
|
-
|
|
158
|
-
|
|
320
|
+
const wantsList = args.options.has("list");
|
|
321
|
+
const requestedActions = [
|
|
322
|
+
installValue ? "install" : null,
|
|
323
|
+
setupValue ? "setup" : null,
|
|
324
|
+
statusValue ? "status" : null,
|
|
325
|
+
uninstallValue ? "uninstall" : null,
|
|
326
|
+
wantsList ? "list" : null,
|
|
327
|
+
].filter(Boolean);
|
|
328
|
+
if (requestedActions.length > 1) {
|
|
329
|
+
throw new Error("Use apenas uma acao por vez: --install, --setup, --status, --uninstall ou --list.");
|
|
159
330
|
}
|
|
160
331
|
if (installValue) {
|
|
332
|
+
const installSlugs = normalizeInstalledExtensions(installValue.split(","));
|
|
161
333
|
const nextExtensions = normalizeInstalledExtensions([
|
|
162
334
|
...config.installedExtensions,
|
|
163
|
-
...
|
|
335
|
+
...installSlugs,
|
|
164
336
|
]);
|
|
165
337
|
const added = nextExtensions.filter((item) => !config.installedExtensions.includes(item));
|
|
166
338
|
if (!added.length) {
|
|
@@ -171,10 +343,74 @@ async function runExtensionsCommand(args) {
|
|
|
171
343
|
...config,
|
|
172
344
|
installedExtensions: nextExtensions,
|
|
173
345
|
});
|
|
346
|
+
for (const extension of added) {
|
|
347
|
+
if (!isManagedBridgeExtensionSlug(extension)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const state = await buildInstalledManagedExtensionState(extension);
|
|
351
|
+
await saveManagedBridgeExtensionState(extension, state);
|
|
352
|
+
}
|
|
174
353
|
console.log(`[otto-bridge] extensoes instaladas: ${added.join(", ")}`);
|
|
354
|
+
for (const extension of added) {
|
|
355
|
+
if (!isManagedBridgeExtensionSlug(extension)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
console.log(`[otto-bridge] proximo passo para ${extension}: otto-bridge extensions --setup ${extension}`);
|
|
359
|
+
}
|
|
175
360
|
console.log("[otto-bridge] rode `otto-bridge run` novamente se quiser sincronizar agora com a web");
|
|
176
361
|
return;
|
|
177
362
|
}
|
|
363
|
+
if (setupValue) {
|
|
364
|
+
const slug = normalizeInstalledExtensions([setupValue])[0];
|
|
365
|
+
if (!slug) {
|
|
366
|
+
throw new Error("Informe uma extensao valida em --setup <slug>.");
|
|
367
|
+
}
|
|
368
|
+
if (!config.installedExtensions.includes(slug)) {
|
|
369
|
+
throw new Error(`A extensao ${slug} nao esta instalada. Rode \`otto-bridge extensions --install ${slug}\` primeiro.`);
|
|
370
|
+
}
|
|
371
|
+
if (!isManagedBridgeExtensionSlug(slug)) {
|
|
372
|
+
throw new Error(`A extensao ${slug} nao possui setup interativo gerenciado no bridge.`);
|
|
373
|
+
}
|
|
374
|
+
const definition = getManagedBridgeExtensionDefinition(slug);
|
|
375
|
+
const currentState = await buildInstalledManagedExtensionState(slug);
|
|
376
|
+
await saveManagedBridgeExtensionState(slug, {
|
|
377
|
+
...currentState,
|
|
378
|
+
status: "waiting_login",
|
|
379
|
+
lastSetupAt: new Date().toISOString(),
|
|
380
|
+
notes: `Setup iniciado para ${definition.displayName}. Escaneie o QR code no Safari e depois rode \`otto-bridge extensions --status ${slug}\`.`,
|
|
381
|
+
});
|
|
382
|
+
await openManagedExtensionSetup(slug);
|
|
383
|
+
console.log(`[otto-bridge] setup iniciado para ${definition.displayName}`);
|
|
384
|
+
console.log(`[otto-bridge] escaneie o QR code no Safari e depois rode: otto-bridge extensions --status ${slug}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (statusValue) {
|
|
388
|
+
const slug = normalizeInstalledExtensions([statusValue])[0];
|
|
389
|
+
if (!slug) {
|
|
390
|
+
throw new Error("Informe uma extensao valida em --status <slug>.");
|
|
391
|
+
}
|
|
392
|
+
if (!config.installedExtensions.includes(slug)) {
|
|
393
|
+
throw new Error(`A extensao ${slug} nao esta instalada neste bridge.`);
|
|
394
|
+
}
|
|
395
|
+
if (!isManagedBridgeExtensionSlug(slug)) {
|
|
396
|
+
console.log(`[otto-bridge] ${slug}: instalada (sem status gerenciado)`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const currentState = await buildInstalledManagedExtensionState(slug);
|
|
400
|
+
const detected = await detectManagedExtensionStatus(slug, currentState);
|
|
401
|
+
const nextState = {
|
|
402
|
+
...currentState,
|
|
403
|
+
status: detected.status,
|
|
404
|
+
lastStatusCheckAt: new Date().toISOString(),
|
|
405
|
+
notes: detected.notes || currentState.notes,
|
|
406
|
+
};
|
|
407
|
+
await saveManagedBridgeExtensionState(slug, nextState);
|
|
408
|
+
console.log(`[otto-bridge] ${slug}: ${formatManagedBridgeExtensionStatus(nextState.status)}`);
|
|
409
|
+
if (nextState.notes) {
|
|
410
|
+
console.log(`[otto-bridge] ${nextState.notes}`);
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
178
414
|
if (uninstallValue) {
|
|
179
415
|
const removeSet = new Set(normalizeInstalledExtensions(uninstallValue.split(",")));
|
|
180
416
|
const nextExtensions = config.installedExtensions.filter((item) => !removeSet.has(item));
|
|
@@ -187,6 +423,11 @@ async function runExtensionsCommand(args) {
|
|
|
187
423
|
...config,
|
|
188
424
|
installedExtensions: nextExtensions,
|
|
189
425
|
});
|
|
426
|
+
for (const extension of removed) {
|
|
427
|
+
if (isManagedBridgeExtensionSlug(extension)) {
|
|
428
|
+
await removeManagedBridgeExtensionState(extension);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
190
431
|
console.log(`[otto-bridge] extensoes removidas: ${removed.join(", ")}`);
|
|
191
432
|
console.log("[otto-bridge] rode `otto-bridge run` novamente se quiser sincronizar agora com a web");
|
|
192
433
|
return;
|
|
@@ -197,7 +438,13 @@ async function runExtensionsCommand(args) {
|
|
|
197
438
|
}
|
|
198
439
|
console.log("[otto-bridge] extensoes instaladas:");
|
|
199
440
|
for (const extension of config.installedExtensions) {
|
|
200
|
-
|
|
441
|
+
if (!isManagedBridgeExtensionSlug(extension)) {
|
|
442
|
+
console.log(`- ${extension}`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const state = await loadManagedBridgeExtensionState(extension);
|
|
446
|
+
const suffix = state ? ` (${formatManagedBridgeExtensionStatus(state.status)})` : "";
|
|
447
|
+
console.log(`- ${extension}${suffix}`);
|
|
201
448
|
}
|
|
202
449
|
}
|
|
203
450
|
async function runUnpairCommand() {
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.5.
|
|
2
|
+
export const BRIDGE_VERSION = "0.5.14";
|
|
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;
|