bereach-openclaw 1.5.6 → 1.5.7
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.
|
@@ -1289,6 +1289,11 @@ export const definitions: ToolDefinition[] = [
|
|
|
1289
1289
|
dailyActionLimit: { type: "integer", minimum: 1, description: "Max actions per day." },
|
|
1290
1290
|
dailyTarget: { type: "integer", minimum: 1, description: "Daily target goal (e.g. 50 leads/day). Set null to remove." },
|
|
1291
1291
|
totalTarget: { type: "integer", minimum: 1, description: "Total campaign goal (e.g. 500 leads total). Auto-completes when reached. Set null to remove." },
|
|
1292
|
+
handoverMode: {
|
|
1293
|
+
type: "string",
|
|
1294
|
+
enum: ["on_reply", "on_goal", "manual"],
|
|
1295
|
+
description: "When agent hands over conversation to human. on_reply (default): stop autonomous outreach when contact replies. on_goal: keep replying until meeting booked or converted. manual: never auto-stop.",
|
|
1296
|
+
},
|
|
1292
1297
|
taskOverrides: {
|
|
1293
1298
|
type: "object",
|
|
1294
1299
|
description: "Per-task-type config overrides. Keys are task types (e.g. 'lead-gen-visit', 'lead-gen-qualify'). Values are config objects with: maxRunsPerDay, minIntervalMinutes, defaultBatchSize, maxCreditsPerRun.",
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -63,6 +63,14 @@ type TaskResult = {
|
|
|
63
63
|
|
|
64
64
|
const MIN_POLL_MS = 5000;
|
|
65
65
|
|
|
66
|
+
/** Thrown when API returns 401/403 - credentials are invalid, retrying won't help */
|
|
67
|
+
export class AuthError extends Error {
|
|
68
|
+
constructor(public readonly statusCode: number, body: string) {
|
|
69
|
+
super(`Auth failed (HTTP ${statusCode}): ${body.slice(0, 200)}`);
|
|
70
|
+
this.name = "AuthError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
// ---------------------------------------------------------------------------
|
|
67
75
|
// TaskResult extraction from OpenClaw agent output
|
|
68
76
|
// ---------------------------------------------------------------------------
|
|
@@ -167,6 +175,9 @@ async function httpPost(url: string, body: unknown, headers: Record<string, stri
|
|
|
167
175
|
});
|
|
168
176
|
if (!res.ok) {
|
|
169
177
|
const text = await res.text().catch(() => "");
|
|
178
|
+
if (res.status === 401 || res.status === 403) {
|
|
179
|
+
throw new AuthError(res.status, text);
|
|
180
|
+
}
|
|
170
181
|
throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
171
182
|
}
|
|
172
183
|
return res.json();
|
|
@@ -513,7 +524,7 @@ async function updateTaskStatus(
|
|
|
513
524
|
}
|
|
514
525
|
|
|
515
526
|
export type ConnectorStatus = {
|
|
516
|
-
state: "stopped" | "starting" | "running" | "stopping" | "error" | "disabled";
|
|
527
|
+
state: "stopped" | "starting" | "running" | "stopping" | "error" | "disabled" | "dormant";
|
|
517
528
|
uptime: number;
|
|
518
529
|
lastHeartbeat: number;
|
|
519
530
|
currentTaskId: string | undefined;
|
|
@@ -524,16 +535,26 @@ export type ConnectorStatus = {
|
|
|
524
535
|
|
|
525
536
|
export async function runConnectorLoop(
|
|
526
537
|
config: ConnectorConfig,
|
|
527
|
-
options?: { signal?: AbortSignal },
|
|
538
|
+
options?: { signal?: AbortSignal; onHeartbeat?: () => void },
|
|
528
539
|
): Promise<void> {
|
|
529
540
|
let pollInterval = config.pollIntervalMs;
|
|
530
541
|
let currentTaskId: string | undefined;
|
|
531
542
|
let consecutiveErrors = 0;
|
|
543
|
+
let consecutiveAuthErrors = 0;
|
|
532
544
|
let totalTasksExecuted = 0;
|
|
533
545
|
let webhookAvailable = false;
|
|
534
546
|
|
|
535
547
|
const signal = options?.signal;
|
|
536
548
|
|
|
549
|
+
// Internal AbortController: links heartbeat + poll loops so either can abort both.
|
|
550
|
+
// When heartbeat detects auth failure → aborts internal → poll loop exits too.
|
|
551
|
+
// External signal (from ConnectorManager) is forwarded to internal.
|
|
552
|
+
const internalAbort = new AbortController();
|
|
553
|
+
const internalSignal = internalAbort.signal;
|
|
554
|
+
if (signal) {
|
|
555
|
+
signal.addEventListener("abort", () => internalAbort.abort(signal.reason), { once: true });
|
|
556
|
+
}
|
|
557
|
+
|
|
537
558
|
console.log(`[connector] Starting connector loop`);
|
|
538
559
|
console.log(`[connector] API: ${config.apiUrl}`);
|
|
539
560
|
console.log(`[connector] Auth: ${config.apiKey ? "API key (Bearer)" : "connector token (legacy)"}`);
|
|
@@ -550,32 +571,47 @@ export async function runConnectorLoop(
|
|
|
550
571
|
|
|
551
572
|
// Heartbeat runs independently - does NOT touch pollInterval or consecutiveErrors.
|
|
552
573
|
// Otherwise it would neutralize exponential backoff during error recovery.
|
|
574
|
+
// Uses internalAbort so auth failures kill both heartbeat AND poll loops.
|
|
553
575
|
let consecutiveHeartbeatFailures = 0;
|
|
576
|
+
let consecutiveHeartbeatAuthErrors = 0;
|
|
554
577
|
const heartbeatLoop = async () => {
|
|
555
|
-
|
|
578
|
+
let heartbeatIntervalMs = 30_000;
|
|
579
|
+
while (!internalSignal.aborted) {
|
|
556
580
|
try {
|
|
557
581
|
await heartbeat(config, currentTaskId);
|
|
558
582
|
consecutiveHeartbeatFailures = 0;
|
|
583
|
+
consecutiveHeartbeatAuthErrors = 0;
|
|
584
|
+
heartbeatIntervalMs = 30_000;
|
|
585
|
+
options?.onHeartbeat?.();
|
|
559
586
|
} catch (err) {
|
|
560
587
|
consecutiveHeartbeatFailures++;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
console.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
588
|
+
if (err instanceof AuthError) {
|
|
589
|
+
consecutiveHeartbeatAuthErrors++;
|
|
590
|
+
console.error(`[connector] Heartbeat auth failed (${consecutiveHeartbeatAuthErrors}/3): ${err.message}`);
|
|
591
|
+
if (consecutiveHeartbeatAuthErrors >= 3) {
|
|
592
|
+
console.error("[connector] FATAL: 3 consecutive auth failures in heartbeat, API key is invalid");
|
|
593
|
+
internalAbort.abort(err);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
consecutiveHeartbeatAuthErrors = 0;
|
|
598
|
+
console.error(`[connector] Heartbeat failed (${consecutiveHeartbeatFailures}):`, err instanceof Error ? err.message : String(err));
|
|
599
|
+
if (consecutiveHeartbeatFailures === 5) {
|
|
600
|
+
console.warn("[connector] WARNING: 5 consecutive heartbeat failures, API may be unreachable");
|
|
601
|
+
}
|
|
602
|
+
// Exponential backoff: 30s → 60s → 120s → max 5min (never die for transient errors)
|
|
603
|
+
heartbeatIntervalMs = Math.min(heartbeatIntervalMs * 2, 5 * 60 * 1000);
|
|
569
604
|
}
|
|
570
605
|
}
|
|
571
606
|
await new Promise((r) => {
|
|
572
|
-
const timer = setTimeout(r,
|
|
573
|
-
|
|
607
|
+
const timer = setTimeout(r, heartbeatIntervalMs);
|
|
608
|
+
internalSignal.addEventListener("abort", () => { clearTimeout(timer); r(undefined); }, { once: true });
|
|
574
609
|
});
|
|
575
610
|
}
|
|
576
611
|
};
|
|
577
612
|
heartbeatLoop().catch((err) => {
|
|
578
613
|
console.error("[connector] Heartbeat loop crashed:", err instanceof Error ? err.message : String(err));
|
|
614
|
+
if (!internalSignal.aborted) internalAbort.abort(err);
|
|
579
615
|
});
|
|
580
616
|
|
|
581
617
|
// Task execution lock - prevents concurrent task execution.
|
|
@@ -583,11 +619,11 @@ export async function runConnectorLoop(
|
|
|
583
619
|
let isExecuting = false;
|
|
584
620
|
let idlePollCount = 0;
|
|
585
621
|
|
|
586
|
-
while (!
|
|
622
|
+
while (!internalSignal.aborted) {
|
|
587
623
|
try {
|
|
588
624
|
// Skip polling while a task is running - prevents claiming concurrent tasks
|
|
589
625
|
if (isExecuting) {
|
|
590
|
-
await sleep(pollInterval,
|
|
626
|
+
await sleep(pollInterval, internalSignal);
|
|
591
627
|
continue;
|
|
592
628
|
}
|
|
593
629
|
|
|
@@ -691,10 +727,22 @@ export async function runConnectorLoop(
|
|
|
691
727
|
isExecuting = false;
|
|
692
728
|
currentTaskId = undefined;
|
|
693
729
|
(globalThis as any).__bereachCurrentTaskId = undefined;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
730
|
+
|
|
731
|
+
if (err instanceof AuthError) {
|
|
732
|
+
consecutiveAuthErrors++;
|
|
733
|
+
console.error(`[connector] Poll auth failed (${consecutiveAuthErrors}/3): ${err.message}`);
|
|
734
|
+
if (consecutiveAuthErrors >= 3) {
|
|
735
|
+
console.error("[connector] FATAL: 3 consecutive auth failures in poll loop, API key is invalid");
|
|
736
|
+
internalAbort.abort(err);
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
consecutiveAuthErrors = 0;
|
|
741
|
+
console.error(
|
|
742
|
+
`[connector] Poll error (${consecutiveErrors}):`,
|
|
743
|
+
err instanceof Error ? err.message : String(err),
|
|
744
|
+
);
|
|
745
|
+
}
|
|
698
746
|
|
|
699
747
|
pollInterval = Math.min(
|
|
700
748
|
config.pollIntervalMs * Math.pow(2, consecutiveErrors),
|
|
@@ -711,10 +759,17 @@ export async function runConnectorLoop(
|
|
|
711
759
|
}
|
|
712
760
|
}
|
|
713
761
|
|
|
714
|
-
await sleep(pollInterval,
|
|
762
|
+
await sleep(pollInterval, internalSignal);
|
|
715
763
|
}
|
|
716
764
|
|
|
717
765
|
console.log("[connector] Loop stopped (abort signal received)");
|
|
766
|
+
|
|
767
|
+
// If internal abort fired (auth failure) but external didn't (not a graceful shutdown),
|
|
768
|
+
// throw so ConnectorManager.handleCrash() triggers with the auth error.
|
|
769
|
+
if (internalSignal.aborted && !signal?.aborted) {
|
|
770
|
+
const reason = internalSignal.reason;
|
|
771
|
+
throw reason instanceof Error ? reason : new Error("Connector loop aborted internally");
|
|
772
|
+
}
|
|
718
773
|
}
|
|
719
774
|
|
|
720
775
|
/** Re-probe webhook availability. Called by ConnectorManager on retry. */
|
package/src/connector/manager.ts
CHANGED
|
@@ -4,39 +4,75 @@
|
|
|
4
4
|
* Auto-starts the polling loop from register() when an API key is present.
|
|
5
5
|
* Handles crash recovery with exponential backoff, graceful shutdown on
|
|
6
6
|
* SIGTERM/SIGINT, and prevents duplicate instances.
|
|
7
|
+
*
|
|
8
|
+
* Key resilience features:
|
|
9
|
+
* - AuthError detection: distinguishes auth failures from transient errors
|
|
10
|
+
* - Dormant mode: after persistent auth failures, enters low-power probe mode
|
|
11
|
+
* instead of burning restarts. Probes every 15 min and auto-recovers.
|
|
12
|
+
* - Config resolver: re-resolves API key on each restart/probe
|
|
13
|
+
* - Health check: detects zombie state (running but heartbeat dead)
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
|
-
import { runConnectorLoop, isWebhookAvailable, type ConnectorConfig, type ConnectorStatus } from "../commands/connector.js";
|
|
16
|
+
import { runConnectorLoop, isWebhookAvailable, AuthError, type ConnectorConfig, type ConnectorStatus } from "../commands/connector.js";
|
|
10
17
|
|
|
11
18
|
const MAX_RESTARTS = 10;
|
|
12
19
|
const INITIAL_RESTART_DELAY_MS = 1_000;
|
|
13
20
|
const MAX_RESTART_DELAY_MS = 30_000;
|
|
21
|
+
const DORMANT_PROBE_INTERVAL_MS = 15 * 60 * 1000; // 15 min
|
|
14
22
|
|
|
15
|
-
export type ConnectorManagerState = "stopped" | "starting" | "running" | "stopping" | "error" | "disabled";
|
|
23
|
+
export type ConnectorManagerState = "stopped" | "starting" | "running" | "stopping" | "error" | "disabled" | "dormant";
|
|
16
24
|
|
|
17
25
|
class ConnectorManager {
|
|
18
26
|
private state: ConnectorManagerState = "stopped";
|
|
19
27
|
private abortController: AbortController | null = null;
|
|
20
28
|
private config: ConnectorConfig | null = null;
|
|
29
|
+
private resolveConfig: (() => ConnectorConfig) | null = null;
|
|
21
30
|
private restartCount = 0;
|
|
22
31
|
private startedAt = 0;
|
|
23
32
|
private lastHeartbeatAt = 0;
|
|
24
33
|
private totalTasksExecuted = 0;
|
|
25
34
|
private consecutiveErrors = 0;
|
|
35
|
+
private consecutiveAuthCrashes = 0;
|
|
26
36
|
private executionMode: "webhook" | "execFile" = "execFile";
|
|
27
37
|
private loopPromise: Promise<void> | null = null;
|
|
28
38
|
private signalHandlersInstalled = false;
|
|
39
|
+
private stabilityTimer: ReturnType<typeof setTimeout> | null = null;
|
|
40
|
+
private dormantTimer: ReturnType<typeof setTimeout> | null = null;
|
|
29
41
|
|
|
30
42
|
/**
|
|
31
|
-
* Start the connector.
|
|
43
|
+
* Start the connector. Accepts either a static config or a resolver function
|
|
44
|
+
* that re-resolves config (including API key) on each restart.
|
|
45
|
+
*
|
|
46
|
+
* Idempotent with smart recovery:
|
|
47
|
+
* - If dormant: wakes up and attempts restart (user may have changed key)
|
|
48
|
+
* - If running but unhealthy: forces restart
|
|
49
|
+
* - If running and healthy: skips
|
|
32
50
|
*/
|
|
33
|
-
async start(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
async start(configOrResolver: ConnectorConfig | (() => ConnectorConfig)): Promise<void> {
|
|
52
|
+
// Store resolver for future re-resolution
|
|
53
|
+
if (typeof configOrResolver === "function") {
|
|
54
|
+
this.resolveConfig = configOrResolver;
|
|
55
|
+
this.config = configOrResolver();
|
|
56
|
+
} else {
|
|
57
|
+
this.resolveConfig = null;
|
|
58
|
+
this.config = configOrResolver;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.state === "dormant") {
|
|
62
|
+
// User may have changed their key - wake up and try
|
|
63
|
+
console.log("[bereach:connector] waking from dormant mode (register() called)");
|
|
64
|
+
if (this.dormantTimer) { clearTimeout(this.dormantTimer); this.dormantTimer = null; }
|
|
65
|
+
this.consecutiveAuthCrashes = 0;
|
|
66
|
+
// Fall through to normal start
|
|
67
|
+
} else if (this.state === "running" || this.state === "starting") {
|
|
68
|
+
if (this.isHealthy()) {
|
|
69
|
+
console.log("[bereach:connector] skip duplicate start (already running and healthy)");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.warn("[bereach:connector] connector unhealthy, forcing restart");
|
|
73
|
+
await this.stop();
|
|
37
74
|
}
|
|
38
75
|
|
|
39
|
-
this.config = config;
|
|
40
76
|
this.state = "starting";
|
|
41
77
|
this.restartCount = 0;
|
|
42
78
|
|
|
@@ -73,11 +109,11 @@ class ConnectorManager {
|
|
|
73
109
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
74
110
|
}
|
|
75
111
|
|
|
76
|
-
// Probe webhook availability with retry
|
|
112
|
+
// Probe webhook availability with retry - the gateway HTTP server may not be
|
|
77
113
|
// listening yet when register() fires during plugin load.
|
|
78
114
|
let webhookOk = false;
|
|
79
115
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
80
|
-
webhookOk = await isWebhookAvailable(config);
|
|
116
|
+
webhookOk = await isWebhookAvailable(this.config);
|
|
81
117
|
if (webhookOk) break;
|
|
82
118
|
if (attempt < 5) {
|
|
83
119
|
console.log(`[bereach:connector] webhook probe attempt ${attempt}/5 failed, retrying in 3s...`);
|
|
@@ -95,6 +131,9 @@ class ConnectorManager {
|
|
|
95
131
|
async stop(): Promise<void> {
|
|
96
132
|
if (this.state === "stopped" || this.state === "disabled") return;
|
|
97
133
|
|
|
134
|
+
// Clean up dormant timer if active
|
|
135
|
+
if (this.dormantTimer) { clearTimeout(this.dormantTimer); this.dormantTimer = null; }
|
|
136
|
+
|
|
98
137
|
this.state = "stopping";
|
|
99
138
|
console.log("[bereach:connector] stopping...");
|
|
100
139
|
|
|
@@ -128,10 +167,43 @@ class ConnectorManager {
|
|
|
128
167
|
};
|
|
129
168
|
}
|
|
130
169
|
|
|
131
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Check if the connector is actually healthy (not a zombie).
|
|
172
|
+
* Unhealthy = running state but no heartbeat in 5+ minutes,
|
|
173
|
+
* or running for 2+ minutes without ever getting a heartbeat.
|
|
174
|
+
*/
|
|
175
|
+
private isHealthy(): boolean {
|
|
176
|
+
if (this.state !== "running") return false;
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
const uptime = now - this.startedAt;
|
|
179
|
+
// Just started (< 2 min), give it time
|
|
180
|
+
if (uptime < 2 * 60 * 1000) return true;
|
|
181
|
+
// No heartbeat ever received after 2 min = zombie
|
|
182
|
+
if (!this.lastHeartbeatAt) return false;
|
|
183
|
+
// Last heartbeat > 5 min ago = zombie
|
|
184
|
+
return (now - this.lastHeartbeatAt) < 5 * 60 * 1000;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Re-resolve config from the resolver (picks up API key changes).
|
|
189
|
+
* Falls back to stored config if no resolver.
|
|
190
|
+
*/
|
|
191
|
+
private freshConfig(): ConnectorConfig | null {
|
|
192
|
+
if (this.resolveConfig) {
|
|
193
|
+
try {
|
|
194
|
+
this.config = this.resolveConfig();
|
|
195
|
+
const masked = this.config.apiKey ? `...${this.config.apiKey.slice(-6)}` : "NOT SET";
|
|
196
|
+
console.log(`[bereach:connector] re-resolved config (API key: ${masked})`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error("[bereach:connector] config resolver failed:", err instanceof Error ? err.message : String(err));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return this.config;
|
|
202
|
+
}
|
|
132
203
|
|
|
133
204
|
private startLoop(): void {
|
|
134
|
-
|
|
205
|
+
const config = this.freshConfig();
|
|
206
|
+
if (!config) return;
|
|
135
207
|
|
|
136
208
|
this.abortController = new AbortController();
|
|
137
209
|
this.startedAt = Date.now();
|
|
@@ -151,7 +223,10 @@ class ConnectorManager {
|
|
|
151
223
|
}, 5 * 60 * 1000);
|
|
152
224
|
this.stabilityTimer.unref();
|
|
153
225
|
|
|
154
|
-
this.loopPromise = runConnectorLoop(
|
|
226
|
+
this.loopPromise = runConnectorLoop(config, {
|
|
227
|
+
signal: this.abortController.signal,
|
|
228
|
+
onHeartbeat: () => { this.lastHeartbeatAt = Date.now(); },
|
|
229
|
+
})
|
|
155
230
|
.then(() => {
|
|
156
231
|
// Normal exit (abort signal)
|
|
157
232
|
if (this.state !== "stopping") {
|
|
@@ -160,14 +235,29 @@ class ConnectorManager {
|
|
|
160
235
|
})
|
|
161
236
|
.catch((err) => {
|
|
162
237
|
console.error(`[bereach:connector] loop crashed: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
|
-
this.handleCrash();
|
|
238
|
+
this.handleCrash(err);
|
|
164
239
|
});
|
|
165
240
|
}
|
|
166
241
|
|
|
167
|
-
private handleCrash(): void {
|
|
242
|
+
private handleCrash(err?: unknown): void {
|
|
168
243
|
this.restartCount++;
|
|
169
244
|
this.consecutiveErrors++;
|
|
170
245
|
|
|
246
|
+
// Track consecutive auth-related crashes for dormant mode.
|
|
247
|
+
// 2 auth crashes = 2 full start→3-failures→crash cycles = ~6 total 401s.
|
|
248
|
+
const isAuthCrash = err instanceof AuthError ||
|
|
249
|
+
(err instanceof Error && err.message.includes("Auth failed"));
|
|
250
|
+
if (isAuthCrash) {
|
|
251
|
+
this.consecutiveAuthCrashes++;
|
|
252
|
+
console.error(`[bereach:connector] auth crash ${this.consecutiveAuthCrashes}/2`);
|
|
253
|
+
if (this.consecutiveAuthCrashes >= 2) {
|
|
254
|
+
this.enterDormant();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
this.consecutiveAuthCrashes = 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
171
261
|
if (this.restartCount > MAX_RESTARTS) {
|
|
172
262
|
// Don't give up permanently - enter cooldown then restart.
|
|
173
263
|
// The user's VPS has no supervisor to restart us, so we must self-recover.
|
|
@@ -199,6 +289,69 @@ class ConnectorManager {
|
|
|
199
289
|
this.startLoop();
|
|
200
290
|
}, delay);
|
|
201
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Enter dormant mode - lightweight 15-min probe instead of full restart loop.
|
|
295
|
+
* Used when API key is confirmed invalid (persistent auth failures).
|
|
296
|
+
* Auto-recovers when the key becomes valid again.
|
|
297
|
+
*/
|
|
298
|
+
private enterDormant(): void {
|
|
299
|
+
this.state = "dormant";
|
|
300
|
+
this.abortController = null;
|
|
301
|
+
this.loopPromise = null;
|
|
302
|
+
console.error(
|
|
303
|
+
"[bereach:connector] API key invalid - entering dormant mode. " +
|
|
304
|
+
"Will probe every 15 min and auto-recover when key is fixed.",
|
|
305
|
+
);
|
|
306
|
+
this.scheduleDormantProbe();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private scheduleDormantProbe(): void {
|
|
310
|
+
if (this.dormantTimer) clearTimeout(this.dormantTimer);
|
|
311
|
+
this.dormantTimer = setTimeout(() => this.runDormantProbe(), DORMANT_PROBE_INTERVAL_MS);
|
|
312
|
+
this.dormantTimer.unref();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async runDormantProbe(): Promise<void> {
|
|
316
|
+
if (this.state !== "dormant") return;
|
|
317
|
+
|
|
318
|
+
// Re-resolve config to pick up any key changes
|
|
319
|
+
const config = this.freshConfig();
|
|
320
|
+
if (!config?.apiKey) {
|
|
321
|
+
console.log("[bereach:connector] dormant probe: no API key configured, will retry in 15 min");
|
|
322
|
+
this.scheduleDormantProbe();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Single lightweight heartbeat probe
|
|
327
|
+
try {
|
|
328
|
+
const res = await fetch(`${config.apiUrl}/connectors/heartbeat`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: {
|
|
331
|
+
"Content-Type": "application/json",
|
|
332
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify({}),
|
|
335
|
+
signal: AbortSignal.timeout(10_000),
|
|
336
|
+
});
|
|
337
|
+
if (res.ok) {
|
|
338
|
+
console.log("[bereach:connector] dormant probe succeeded! Restarting connector.");
|
|
339
|
+
this.consecutiveAuthCrashes = 0;
|
|
340
|
+
this.restartCount = 0;
|
|
341
|
+
this.consecutiveErrors = 0;
|
|
342
|
+
this.startLoop();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (res.status === 401 || res.status === 403) {
|
|
346
|
+
console.log("[bereach:connector] dormant probe: still 401, will retry in 15 min");
|
|
347
|
+
} else {
|
|
348
|
+
console.log(`[bereach:connector] dormant probe: HTTP ${res.status}, will retry in 15 min`);
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.log(`[bereach:connector] dormant probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
352
|
+
}
|
|
353
|
+
this.scheduleDormantProbe();
|
|
354
|
+
}
|
|
202
355
|
}
|
|
203
356
|
|
|
204
357
|
// Module-level singleton
|
package/src/index.ts
CHANGED
|
@@ -232,14 +232,14 @@ export default function register(api: any) {
|
|
|
232
232
|
const gatewayUrl = config.gatewayUrl || readEnv("OPENCLAW_GATEWAY_URL") || "http://localhost:18789";
|
|
233
233
|
const pollMs = config.connectorPollIntervalMs ?? 15000;
|
|
234
234
|
|
|
235
|
-
connectorManager.start({
|
|
236
|
-
apiKey,
|
|
235
|
+
connectorManager.start(() => ({
|
|
236
|
+
apiKey: resolveApiKey(api),
|
|
237
237
|
apiUrl: API_BASE,
|
|
238
238
|
openclawUrl: readEnv("OPENCLAW_URL") || "http://localhost:3579",
|
|
239
239
|
pollIntervalMs: Math.max(5000, pollMs),
|
|
240
240
|
gatewayUrl,
|
|
241
241
|
hooksToken,
|
|
242
|
-
}).catch((err) => {
|
|
242
|
+
})).catch((err) => {
|
|
243
243
|
log(`connector auto-start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
244
244
|
});
|
|
245
245
|
} else if (!connectorEnabled) {
|