@leg3ndy/otto-bridge 0.6.1 → 0.6.3
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 +9 -3
- package/dist/executors/native_macos.js +77 -7
- package/dist/extensions.js +2 -0
- package/dist/main.js +196 -6
- package/dist/runtime.js +45 -1
- package/dist/types.js +1 -1
- package/dist/whatsapp_background.js +32 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
npm pack
|
|
36
|
-
npm install -g ./leg3ndy-otto-bridge-0.6.
|
|
36
|
+
npm install -g ./leg3ndy-otto-bridge-0.6.3.tgz
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
No `0.6.
|
|
39
|
+
No `0.6.3`, `playwright` deixa de ser opcional no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
|
|
40
40
|
|
|
41
41
|
## Publicacao
|
|
42
42
|
|
|
@@ -106,7 +106,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
106
106
|
|
|
107
107
|
### WhatsApp Web em background
|
|
108
108
|
|
|
109
|
-
Fluxo recomendado no `0.6.
|
|
109
|
+
Fluxo recomendado no `0.6.3`:
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
otto-bridge extensions --install whatsappweb
|
|
@@ -116,6 +116,12 @@ otto-bridge extensions --status whatsappweb
|
|
|
116
116
|
|
|
117
117
|
O setup agora abre o login do WhatsApp Web em um browser persistente do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
|
|
118
118
|
|
|
119
|
+
Contrato do `0.6.3`:
|
|
120
|
+
|
|
121
|
+
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
122
|
+
- `otto-bridge run`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime estiver ativo
|
|
123
|
+
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
124
|
+
|
|
119
125
|
### Ver estado local
|
|
120
126
|
|
|
121
127
|
```bash
|
|
@@ -970,9 +970,13 @@ export class NativeMacOSJobExecutor {
|
|
|
970
970
|
lastVisualTargetDescription = null;
|
|
971
971
|
lastVisualTargetApp = null;
|
|
972
972
|
whatsappBackgroundBrowser = null;
|
|
973
|
+
whatsappRuntimeMonitor = null;
|
|
973
974
|
constructor(bridgeConfig) {
|
|
974
975
|
this.bridgeConfig = bridgeConfig;
|
|
975
976
|
}
|
|
977
|
+
async start() {
|
|
978
|
+
await this.ensureManagedBackgroundServices();
|
|
979
|
+
}
|
|
976
980
|
async run(job, reporter) {
|
|
977
981
|
if (process.platform !== "darwin") {
|
|
978
982
|
throw new Error("The native-macos executor only runs on macOS");
|
|
@@ -1481,7 +1485,6 @@ export class NativeMacOSJobExecutor {
|
|
|
1481
1485
|
await reporter.completed(resultPayload);
|
|
1482
1486
|
}
|
|
1483
1487
|
finally {
|
|
1484
|
-
await this.closeWhatsAppBackgroundBrowser();
|
|
1485
1488
|
this.cancelledJobs.delete(job.job_id);
|
|
1486
1489
|
}
|
|
1487
1490
|
}
|
|
@@ -1492,6 +1495,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1492
1495
|
this.activeChild = null;
|
|
1493
1496
|
}
|
|
1494
1497
|
}
|
|
1498
|
+
async close() {
|
|
1499
|
+
if (this.whatsappRuntimeMonitor) {
|
|
1500
|
+
clearInterval(this.whatsappRuntimeMonitor);
|
|
1501
|
+
this.whatsappRuntimeMonitor = null;
|
|
1502
|
+
}
|
|
1503
|
+
await this.syncWhatsAppRuntimeDetached();
|
|
1504
|
+
await this.closeWhatsAppBackgroundBrowser();
|
|
1505
|
+
}
|
|
1495
1506
|
assertNotCancelled(jobId) {
|
|
1496
1507
|
if (this.cancelledJobs.has(jobId)) {
|
|
1497
1508
|
throw new JobCancelledError(jobId);
|
|
@@ -2115,16 +2126,32 @@ end repeat
|
|
|
2115
2126
|
activate,
|
|
2116
2127
|
};
|
|
2117
2128
|
}
|
|
2118
|
-
async syncWhatsAppExtensionState(status, notes) {
|
|
2129
|
+
async syncWhatsAppExtensionState(status, notes, options) {
|
|
2119
2130
|
const current = await loadManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG).catch(() => null);
|
|
2120
2131
|
if (!current) {
|
|
2121
2132
|
return;
|
|
2122
2133
|
}
|
|
2134
|
+
const nowIso = new Date().toISOString();
|
|
2123
2135
|
await saveManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG, {
|
|
2124
2136
|
...current,
|
|
2125
2137
|
status,
|
|
2126
2138
|
notes,
|
|
2127
|
-
|
|
2139
|
+
runtimeAttached: options?.runtimeAttached ?? current.runtimeAttached,
|
|
2140
|
+
lastRuntimeHeartbeatAt: options?.runtimeAttached !== undefined
|
|
2141
|
+
? nowIso
|
|
2142
|
+
: current.lastRuntimeHeartbeatAt,
|
|
2143
|
+
lastStatusCheckAt: nowIso,
|
|
2144
|
+
}).catch(() => undefined);
|
|
2145
|
+
}
|
|
2146
|
+
async syncWhatsAppRuntimeDetached() {
|
|
2147
|
+
const current = await loadManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG).catch(() => null);
|
|
2148
|
+
if (!current) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
await saveManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG, {
|
|
2152
|
+
...current,
|
|
2153
|
+
runtimeAttached: false,
|
|
2154
|
+
lastRuntimeHeartbeatAt: new Date().toISOString(),
|
|
2128
2155
|
}).catch(() => undefined);
|
|
2129
2156
|
}
|
|
2130
2157
|
async getWhatsAppBackgroundBrowser() {
|
|
@@ -2191,9 +2218,9 @@ return {
|
|
|
2191
2218
|
const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
|
|
2192
2219
|
if (backgroundBrowser) {
|
|
2193
2220
|
try {
|
|
2194
|
-
const state = await backgroundBrowser.
|
|
2221
|
+
const state = await backgroundBrowser.waitForStableSessionState();
|
|
2195
2222
|
if (state.connected) {
|
|
2196
|
-
await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web pronta para uso no browser em background.");
|
|
2223
|
+
await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web pronta para uso no browser em background.", { runtimeAttached: true });
|
|
2197
2224
|
return;
|
|
2198
2225
|
}
|
|
2199
2226
|
const disconnectedStatus = currentState.status === "connected" || currentState.status === "session_expired"
|
|
@@ -2201,7 +2228,8 @@ return {
|
|
|
2201
2228
|
: "waiting_login";
|
|
2202
2229
|
await this.syncWhatsAppExtensionState(disconnectedStatus, state.qrVisible
|
|
2203
2230
|
? "QR code visivel no browser persistente. Escaneie com o celular para concluir o login."
|
|
2204
|
-
: "Browser persistente do WhatsApp Web aberto, mas a sessao ainda nao esta pronta.");
|
|
2231
|
+
: "Browser persistente do WhatsApp Web aberto, mas a sessao ainda nao esta pronta.", { runtimeAttached: false });
|
|
2232
|
+
await this.closeWhatsAppBackgroundBrowser();
|
|
2205
2233
|
if (disconnectedStatus === "session_expired") {
|
|
2206
2234
|
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`.");
|
|
2207
2235
|
}
|
|
@@ -2261,7 +2289,49 @@ return {
|
|
|
2261
2289
|
}
|
|
2262
2290
|
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`.");
|
|
2263
2291
|
}
|
|
2264
|
-
await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web pronta para uso.");
|
|
2292
|
+
await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web pronta para uso.", { runtimeAttached: false });
|
|
2293
|
+
}
|
|
2294
|
+
async ensureManagedBackgroundServices() {
|
|
2295
|
+
if (!this.hasInstalledBridgeExtension(WHATSAPP_WEB_EXTENSION_SLUG)) {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
if (!this.whatsappRuntimeMonitor) {
|
|
2299
|
+
this.whatsappRuntimeMonitor = setInterval(() => {
|
|
2300
|
+
void this.refreshWhatsAppBackgroundRuntime();
|
|
2301
|
+
}, 60_000);
|
|
2302
|
+
}
|
|
2303
|
+
await this.refreshWhatsAppBackgroundRuntime();
|
|
2304
|
+
}
|
|
2305
|
+
async refreshWhatsAppBackgroundRuntime() {
|
|
2306
|
+
const currentState = await loadManagedBridgeExtensionState(WHATSAPP_WEB_EXTENSION_SLUG).catch(() => null);
|
|
2307
|
+
if (!currentState) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
if (currentState.status !== "connected") {
|
|
2311
|
+
await this.syncWhatsAppRuntimeDetached();
|
|
2312
|
+
await this.closeWhatsAppBackgroundBrowser();
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
|
|
2316
|
+
if (!backgroundBrowser) {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
try {
|
|
2320
|
+
const state = await backgroundBrowser.waitForStableSessionState({ timeoutMs: 12_000 });
|
|
2321
|
+
if (state.connected) {
|
|
2322
|
+
await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web mantida em background enquanto `otto-bridge run` estiver ativo.", { runtimeAttached: true });
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
await this.syncWhatsAppExtensionState("session_expired", state.qrVisible
|
|
2326
|
+
? "A sessao do WhatsApp Web expirou durante o runtime. Rode `otto-bridge extensions --setup whatsappweb` para abrir o QR code novamente."
|
|
2327
|
+
: "O browser persistente do WhatsApp Web nao confirmou uma sessao pronta durante o runtime.", { runtimeAttached: false });
|
|
2328
|
+
await this.closeWhatsAppBackgroundBrowser();
|
|
2329
|
+
}
|
|
2330
|
+
catch (error) {
|
|
2331
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2332
|
+
await this.syncWhatsAppExtensionState("session_expired", detail || "Nao consegui manter a sessao do WhatsApp Web no runtime em background.", { runtimeAttached: false });
|
|
2333
|
+
await this.closeWhatsAppBackgroundBrowser();
|
|
2334
|
+
}
|
|
2265
2335
|
}
|
|
2266
2336
|
async selectWhatsAppConversation(contact) {
|
|
2267
2337
|
const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
|
package/dist/extensions.js
CHANGED
|
@@ -58,6 +58,8 @@ export async function buildInstalledManagedExtensionState(slug) {
|
|
|
58
58
|
installedAt: existing?.installedAt || new Date().toISOString(),
|
|
59
59
|
lastSetupAt: existing?.lastSetupAt,
|
|
60
60
|
lastStatusCheckAt: existing?.lastStatusCheckAt,
|
|
61
|
+
runtimeAttached: existing?.runtimeAttached,
|
|
62
|
+
lastRuntimeHeartbeatAt: existing?.lastRuntimeHeartbeatAt,
|
|
61
63
|
notes: existing?.notes,
|
|
62
64
|
};
|
|
63
65
|
}
|
package/dist/main.js
CHANGED
|
@@ -7,6 +7,8 @@ import { pairDevice } from "./pairing.js";
|
|
|
7
7
|
import { BridgeRuntime } from "./runtime.js";
|
|
8
8
|
import { detectWhatsAppBackgroundStatus, runWhatsAppBackgroundSetup, } from "./whatsapp_background.js";
|
|
9
9
|
import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
|
|
10
|
+
const RUNTIME_STATUS_FRESHNESS_MS = 90_000;
|
|
11
|
+
const UPDATE_RETRY_DELAYS_MS = [0, 8_000, 20_000];
|
|
10
12
|
function parseArgs(argv) {
|
|
11
13
|
const [maybeCommand, ...rest] = argv;
|
|
12
14
|
if (maybeCommand === "--help" || maybeCommand === "-h") {
|
|
@@ -46,6 +48,49 @@ function numberOption(args, name) {
|
|
|
46
48
|
const parsed = Number(value);
|
|
47
49
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
50
|
}
|
|
51
|
+
function hasFreshRuntimeAttachment(state) {
|
|
52
|
+
if (!state?.runtimeAttached || !state.lastRuntimeHeartbeatAt) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const lastHeartbeatAt = Date.parse(state.lastRuntimeHeartbeatAt);
|
|
56
|
+
return Number.isFinite(lastHeartbeatAt) && Date.now() - lastHeartbeatAt <= RUNTIME_STATUS_FRESHNESS_MS;
|
|
57
|
+
}
|
|
58
|
+
function parseSemverTuple(value) {
|
|
59
|
+
const text = String(value || "").trim().replace(/^[vV]/, "");
|
|
60
|
+
if (!text) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const parts = text.split(".");
|
|
64
|
+
const parsed = [];
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
const match = part.match(/^(\d+)/);
|
|
67
|
+
if (!match) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
parsed.push(Number(match[1]));
|
|
71
|
+
}
|
|
72
|
+
return parsed.length > 0 ? parsed : null;
|
|
73
|
+
}
|
|
74
|
+
function compareSemver(left, right) {
|
|
75
|
+
const a = parseSemverTuple(left);
|
|
76
|
+
const b = parseSemverTuple(right);
|
|
77
|
+
if (!a && !b)
|
|
78
|
+
return 0;
|
|
79
|
+
if (!a)
|
|
80
|
+
return -1;
|
|
81
|
+
if (!b)
|
|
82
|
+
return 1;
|
|
83
|
+
const maxLength = Math.max(a.length, b.length);
|
|
84
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
85
|
+
const leftPart = a[index] ?? 0;
|
|
86
|
+
const rightPart = b[index] ?? 0;
|
|
87
|
+
if (leftPart < rightPart)
|
|
88
|
+
return -1;
|
|
89
|
+
if (leftPart > rightPart)
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
49
94
|
function resolveExecutorOverrides(args, current) {
|
|
50
95
|
return resolveExecutorConfig({
|
|
51
96
|
type: option(args, "executor") || process.env.OTTO_BRIDGE_EXECUTOR,
|
|
@@ -100,6 +145,75 @@ function runChildCommand(command, args) {
|
|
|
100
145
|
});
|
|
101
146
|
});
|
|
102
147
|
}
|
|
148
|
+
function runChildCommandCapture(command, args) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const child = spawn(command, args, {
|
|
151
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
env: process.env,
|
|
153
|
+
});
|
|
154
|
+
let stdout = "";
|
|
155
|
+
let stderr = "";
|
|
156
|
+
child.stdout?.setEncoding("utf8");
|
|
157
|
+
child.stdout?.on("data", (chunk) => {
|
|
158
|
+
stdout += String(chunk || "");
|
|
159
|
+
});
|
|
160
|
+
child.stderr?.setEncoding("utf8");
|
|
161
|
+
child.stderr?.on("data", (chunk) => {
|
|
162
|
+
stderr += String(chunk || "");
|
|
163
|
+
});
|
|
164
|
+
child.on("error", (error) => {
|
|
165
|
+
reject(error);
|
|
166
|
+
});
|
|
167
|
+
child.on("exit", (code) => {
|
|
168
|
+
resolve({
|
|
169
|
+
stdout,
|
|
170
|
+
stderr,
|
|
171
|
+
code: code ?? 1,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function resolvePublishedPackageVersion(tag) {
|
|
177
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
178
|
+
const result = await runChildCommandCapture(command, [
|
|
179
|
+
"view",
|
|
180
|
+
`${BRIDGE_PACKAGE_NAME}@${tag}`,
|
|
181
|
+
"version",
|
|
182
|
+
"--json",
|
|
183
|
+
]).catch(() => null);
|
|
184
|
+
if (!result || result.code !== 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const stdout = String(result.stdout || "").trim();
|
|
188
|
+
if (!stdout) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(stdout);
|
|
193
|
+
if (typeof parsed === "string" && parsed.trim()) {
|
|
194
|
+
return parsed.trim();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
if (stdout && stdout !== "null") {
|
|
199
|
+
return stdout.replace(/^"|"$/g, "").trim() || null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
function isRetryableNpmUpdateFailure(detail) {
|
|
205
|
+
const normalized = String(detail || "").toLowerCase();
|
|
206
|
+
return [
|
|
207
|
+
"code etarget",
|
|
208
|
+
"no matching version found",
|
|
209
|
+
"notarget",
|
|
210
|
+
"code enoversions",
|
|
211
|
+
"package version that doesn't exist",
|
|
212
|
+
].some((pattern) => normalized.includes(pattern));
|
|
213
|
+
}
|
|
214
|
+
async function delay(ms) {
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
216
|
+
}
|
|
103
217
|
async function openManagedExtensionSetup(slug) {
|
|
104
218
|
const definition = getManagedBridgeExtensionDefinition(slug);
|
|
105
219
|
if (process.platform !== "darwin") {
|
|
@@ -112,6 +226,12 @@ async function detectManagedWhatsAppWebStatus() {
|
|
|
112
226
|
}
|
|
113
227
|
async function detectManagedExtensionStatus(slug, currentState) {
|
|
114
228
|
if (slug === "whatsappweb") {
|
|
229
|
+
if (hasFreshRuntimeAttachment(currentState)) {
|
|
230
|
+
return {
|
|
231
|
+
status: "connected",
|
|
232
|
+
notes: currentState?.notes || "Sessao local do WhatsApp Web mantida em background enquanto `otto-bridge run` estiver ativo.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
115
235
|
const detected = await detectManagedWhatsAppWebStatus();
|
|
116
236
|
const shouldMarkExpired = (detected.status === "waiting_login"
|
|
117
237
|
&& (currentState?.status === "connected" || currentState?.status === "session_expired"));
|
|
@@ -161,7 +281,31 @@ async function runRuntimeCommand(args) {
|
|
|
161
281
|
executor: resolveExecutorOverrides(args, config.executor),
|
|
162
282
|
};
|
|
163
283
|
const runtime = new BridgeRuntime(runtimeConfig);
|
|
164
|
-
|
|
284
|
+
let stopping = false;
|
|
285
|
+
const shutdown = async (signal) => {
|
|
286
|
+
if (stopping) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
stopping = true;
|
|
290
|
+
console.log(`[otto-bridge] shutting down runtime after ${signal}`);
|
|
291
|
+
await runtime.stop().catch(() => undefined);
|
|
292
|
+
};
|
|
293
|
+
const handleSigint = () => {
|
|
294
|
+
void shutdown("SIGINT");
|
|
295
|
+
};
|
|
296
|
+
const handleSigterm = () => {
|
|
297
|
+
void shutdown("SIGTERM");
|
|
298
|
+
};
|
|
299
|
+
process.once("SIGINT", handleSigint);
|
|
300
|
+
process.once("SIGTERM", handleSigterm);
|
|
301
|
+
try {
|
|
302
|
+
await runtime.start();
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
process.off("SIGINT", handleSigint);
|
|
306
|
+
process.off("SIGTERM", handleSigterm);
|
|
307
|
+
await runtime.stop().catch(() => undefined);
|
|
308
|
+
}
|
|
165
309
|
}
|
|
166
310
|
async function runStatusCommand() {
|
|
167
311
|
const config = await loadBridgeConfig();
|
|
@@ -252,6 +396,7 @@ async function runExtensionsCommand(args) {
|
|
|
252
396
|
};
|
|
253
397
|
await saveManagedBridgeExtensionState(slug, {
|
|
254
398
|
...baseState,
|
|
399
|
+
runtimeAttached: false,
|
|
255
400
|
notes: `Setup iniciado para ${currentState.displayName}. Aguarde o login no browser persistente e depois rode \`otto-bridge extensions --status ${slug}\`.`,
|
|
256
401
|
});
|
|
257
402
|
if (slug === "whatsappweb") {
|
|
@@ -263,6 +408,7 @@ async function runExtensionsCommand(args) {
|
|
|
263
408
|
status: detected.status,
|
|
264
409
|
notes: detected.notes,
|
|
265
410
|
lastStatusCheckAt: new Date().toISOString(),
|
|
411
|
+
runtimeAttached: false,
|
|
266
412
|
});
|
|
267
413
|
console.log(`[otto-bridge] ${slug}: ${formatManagedBridgeExtensionStatus(detected.status)}`);
|
|
268
414
|
if (detected.notes) {
|
|
@@ -293,6 +439,7 @@ async function runExtensionsCommand(args) {
|
|
|
293
439
|
...currentState,
|
|
294
440
|
status: detected.status,
|
|
295
441
|
lastStatusCheckAt: new Date().toISOString(),
|
|
442
|
+
runtimeAttached: hasFreshRuntimeAttachment(currentState) && detected.status === "connected",
|
|
296
443
|
notes: detected.notes || currentState.notes,
|
|
297
444
|
};
|
|
298
445
|
await saveManagedBridgeExtensionState(slug, nextState);
|
|
@@ -344,20 +491,63 @@ async function runUnpairCommand() {
|
|
|
344
491
|
}
|
|
345
492
|
async function runUpdateCommand(args) {
|
|
346
493
|
const tag = option(args, "tag") || "latest";
|
|
347
|
-
const
|
|
494
|
+
const publishedVersion = await resolvePublishedPackageVersion(tag);
|
|
495
|
+
if (publishedVersion && compareSemver(publishedVersion, BRIDGE_VERSION) <= 0) {
|
|
496
|
+
console.log(`[otto-bridge] current=${BRIDGE_VERSION}`);
|
|
497
|
+
console.log(`[otto-bridge] latest published=${publishedVersion}`);
|
|
498
|
+
console.log("[otto-bridge] nenhuma atualizacao mais nova publicada no npm no momento");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const packageSpec = `${BRIDGE_PACKAGE_NAME}@${publishedVersion || tag}`;
|
|
348
502
|
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
349
|
-
const commandArgs = ["install", "-g", packageSpec];
|
|
503
|
+
const commandArgs = ["install", "-g", packageSpec, "--prefer-online"];
|
|
350
504
|
const commandString = `${command} ${commandArgs.join(" ")}`;
|
|
351
505
|
if (args.options.has("dry-run") || args.options.has("check")) {
|
|
352
506
|
console.log(`[otto-bridge] current=${BRIDGE_VERSION}`);
|
|
507
|
+
if (publishedVersion) {
|
|
508
|
+
console.log(`[otto-bridge] target=${publishedVersion}`);
|
|
509
|
+
}
|
|
353
510
|
console.log(`[otto-bridge] update command: ${commandString}`);
|
|
354
511
|
return;
|
|
355
512
|
}
|
|
356
513
|
console.log(`[otto-bridge] current=${BRIDGE_VERSION}`);
|
|
514
|
+
if (publishedVersion) {
|
|
515
|
+
console.log(`[otto-bridge] target=${publishedVersion}`);
|
|
516
|
+
}
|
|
357
517
|
console.log(`[otto-bridge] updating with: ${commandString}`);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
518
|
+
let lastFailureDetail = "";
|
|
519
|
+
for (let attemptIndex = 0; attemptIndex < UPDATE_RETRY_DELAYS_MS.length; attemptIndex += 1) {
|
|
520
|
+
const waitMs = UPDATE_RETRY_DELAYS_MS[attemptIndex];
|
|
521
|
+
if (waitMs > 0) {
|
|
522
|
+
console.log(`[otto-bridge] aguardando ${Math.round(waitMs / 1000)}s para tentar novamente...`);
|
|
523
|
+
await delay(waitMs);
|
|
524
|
+
}
|
|
525
|
+
const result = await runChildCommandCapture(command, commandArgs).catch((error) => ({
|
|
526
|
+
stdout: "",
|
|
527
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
528
|
+
code: 1,
|
|
529
|
+
}));
|
|
530
|
+
if (result.stdout.trim()) {
|
|
531
|
+
process.stdout.write(result.stdout);
|
|
532
|
+
}
|
|
533
|
+
if (result.stderr.trim()) {
|
|
534
|
+
process.stderr.write(result.stderr);
|
|
535
|
+
}
|
|
536
|
+
if (result.code === 0) {
|
|
537
|
+
console.log("[otto-bridge] update completed");
|
|
538
|
+
console.log("[otto-bridge] run `otto-bridge version` to confirm the installed version");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
lastFailureDetail = `${command} exited with code ${result.code}\n${result.stderr || result.stdout}`.trim();
|
|
542
|
+
const retryable = isRetryableNpmUpdateFailure(lastFailureDetail);
|
|
543
|
+
const hasNextAttempt = attemptIndex < UPDATE_RETRY_DELAYS_MS.length - 1;
|
|
544
|
+
if (!retryable || !hasNextAttempt) {
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
console.log("[otto-bridge] A release ainda pode estar propagando no npm. Vou tentar novamente com cache online.");
|
|
548
|
+
}
|
|
549
|
+
const hintSpec = `${BRIDGE_PACKAGE_NAME}@${publishedVersion || tag}`;
|
|
550
|
+
throw new Error(`${lastFailureDetail}\n[otto-bridge] Se a release acabou de ser publicada, isso costuma ser propagacao do npm.\n[otto-bridge] Fallback manual: npm install -g ${hintSpec} --prefer-online`);
|
|
361
551
|
}
|
|
362
552
|
async function main() {
|
|
363
553
|
const args = parseArgs(process.argv.slice(2));
|
package/dist/runtime.js
CHANGED
|
@@ -73,6 +73,9 @@ export class BridgeRuntime {
|
|
|
73
73
|
reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
74
74
|
executor;
|
|
75
75
|
lastBridgeReleaseNoticeKey = null;
|
|
76
|
+
activeSocket = null;
|
|
77
|
+
stopped = false;
|
|
78
|
+
started = false;
|
|
76
79
|
pendingConfirmations = new Map();
|
|
77
80
|
activeCancels = new Map();
|
|
78
81
|
constructor(config, executor) {
|
|
@@ -136,23 +139,62 @@ export class BridgeRuntime {
|
|
|
136
139
|
}));
|
|
137
140
|
}
|
|
138
141
|
async start() {
|
|
142
|
+
if (!this.started) {
|
|
143
|
+
this.started = true;
|
|
144
|
+
if (typeof this.executor.start === "function") {
|
|
145
|
+
await this.executor.start();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
139
148
|
console.log(`[otto-bridge] runtime start device=${this.config.deviceId}`);
|
|
140
|
-
while (
|
|
149
|
+
while (!this.stopped) {
|
|
141
150
|
try {
|
|
142
151
|
await this.connectOnce();
|
|
143
152
|
this.reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
144
153
|
}
|
|
145
154
|
catch (error) {
|
|
155
|
+
if (this.stopped) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
146
158
|
const message = error instanceof Error ? error.message : String(error);
|
|
147
159
|
console.error(`[otto-bridge] socket error: ${message}`);
|
|
148
160
|
}
|
|
161
|
+
if (this.stopped) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
149
164
|
console.log(`[otto-bridge] reconnecting in ${this.reconnectDelayMs}ms`);
|
|
150
165
|
await delay(this.reconnectDelayMs);
|
|
151
166
|
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, DEFAULT_RECONNECT_MAX_DELAY_MS);
|
|
152
167
|
}
|
|
153
168
|
}
|
|
169
|
+
async stop() {
|
|
170
|
+
this.stopped = true;
|
|
171
|
+
for (const [jobId, cancel] of this.activeCancels.entries()) {
|
|
172
|
+
try {
|
|
173
|
+
await cancel();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// ignore shutdown cancellation errors
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
this.activeCancels.delete(jobId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
this.activeSocket?.close();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// ignore socket close errors during shutdown
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
this.activeSocket = null;
|
|
190
|
+
}
|
|
191
|
+
if (typeof this.executor.close === "function") {
|
|
192
|
+
await this.executor.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
154
195
|
async connectOnce() {
|
|
155
196
|
const socket = new WebSocket(this.config.wsUrl, ["device", this.config.deviceToken]);
|
|
197
|
+
this.activeSocket = socket;
|
|
156
198
|
let heartbeatTimer = null;
|
|
157
199
|
const stopHeartbeat = () => {
|
|
158
200
|
if (heartbeatTimer) {
|
|
@@ -196,12 +238,14 @@ export class BridgeRuntime {
|
|
|
196
238
|
socket.addEventListener("close", (event) => {
|
|
197
239
|
stopHeartbeat();
|
|
198
240
|
rejectPendingConfirmations(new Error("WebSocket closed while awaiting confirmation"));
|
|
241
|
+
this.activeSocket = null;
|
|
199
242
|
console.log(`[otto-bridge] socket closed code=${event.code}`);
|
|
200
243
|
resolve();
|
|
201
244
|
});
|
|
202
245
|
socket.addEventListener("error", () => {
|
|
203
246
|
stopHeartbeat();
|
|
204
247
|
rejectPendingConfirmations(new Error("WebSocket failed while awaiting confirmation"));
|
|
248
|
+
this.activeSocket = null;
|
|
205
249
|
try {
|
|
206
250
|
socket.close();
|
|
207
251
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.6.
|
|
2
|
+
export const BRIDGE_VERSION = "0.6.3";
|
|
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;
|
|
@@ -4,6 +4,9 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
4
4
|
import { getBridgeHomeDir } from "./config.js";
|
|
5
5
|
export const WHATSAPP_WEB_URL = "https://web.whatsapp.com";
|
|
6
6
|
const DEFAULT_SETUP_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
|
+
const DEFAULT_SESSION_SETTLE_TIMEOUT_MS = 15_000;
|
|
8
|
+
const DEFAULT_SESSION_POLL_INTERVAL_MS = 1_000;
|
|
9
|
+
const DEFAULT_QR_STABILITY_WINDOW_MS = 4_000;
|
|
7
10
|
function moduleDir() {
|
|
8
11
|
return path.dirname(fileURLToPath(import.meta.url));
|
|
9
12
|
}
|
|
@@ -158,7 +161,7 @@ export class WhatsAppBackgroundBrowser {
|
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
async ensureReady() {
|
|
161
|
-
const state = await this.
|
|
164
|
+
const state = await this.waitForStableSessionState();
|
|
162
165
|
if (state.connected) {
|
|
163
166
|
return state;
|
|
164
167
|
}
|
|
@@ -179,6 +182,33 @@ export class WhatsAppBackgroundBrowser {
|
|
|
179
182
|
}
|
|
180
183
|
return lastState || await this.getSessionState();
|
|
181
184
|
}
|
|
185
|
+
async waitForStableSessionState(options) {
|
|
186
|
+
const timeoutMs = Math.max(2_000, Number(options?.timeoutMs || DEFAULT_SESSION_SETTLE_TIMEOUT_MS));
|
|
187
|
+
const pollIntervalMs = Math.max(250, Number(options?.pollIntervalMs || DEFAULT_SESSION_POLL_INTERVAL_MS));
|
|
188
|
+
const qrStabilityWindowMs = Math.max(pollIntervalMs, Number(options?.qrStabilityWindowMs || DEFAULT_QR_STABILITY_WINDOW_MS));
|
|
189
|
+
const deadline = Date.now() + timeoutMs;
|
|
190
|
+
let lastState = null;
|
|
191
|
+
let qrVisibleSince = null;
|
|
192
|
+
while (Date.now() < deadline) {
|
|
193
|
+
lastState = await this.getSessionState();
|
|
194
|
+
if (lastState.connected) {
|
|
195
|
+
return lastState;
|
|
196
|
+
}
|
|
197
|
+
if (lastState.qrVisible) {
|
|
198
|
+
if (qrVisibleSince === null) {
|
|
199
|
+
qrVisibleSince = Date.now();
|
|
200
|
+
}
|
|
201
|
+
else if (Date.now() - qrVisibleSince >= qrStabilityWindowMs) {
|
|
202
|
+
return lastState;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
qrVisibleSince = null;
|
|
207
|
+
}
|
|
208
|
+
await this.page?.waitForTimeout(pollIntervalMs);
|
|
209
|
+
}
|
|
210
|
+
return lastState || await this.getSessionState();
|
|
211
|
+
}
|
|
182
212
|
async selectConversation(contact) {
|
|
183
213
|
await this.ensureReady();
|
|
184
214
|
const prepared = await this.withPage((page) => page.evaluate((query) => {
|
|
@@ -464,7 +494,7 @@ export async function detectWhatsAppBackgroundStatus() {
|
|
|
464
494
|
}
|
|
465
495
|
const browser = new WhatsAppBackgroundBrowser({ headless: true });
|
|
466
496
|
try {
|
|
467
|
-
const state = await browser.
|
|
497
|
+
const state = await browser.waitForStableSessionState();
|
|
468
498
|
return sessionStateToStatus(state);
|
|
469
499
|
}
|
|
470
500
|
catch (error) {
|