@velanir/openclaw-browserbase 0.1.0 → 0.1.1
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 +10 -5
- package/dist/index.js +139 -37
- package/openclaw.plugin.json +64 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,9 +27,14 @@ browser tool ──cdpUrl http://127.0.0.1:<port>/?token=…──> broker (this
|
|
|
27
27
|
- Every session is created with the coworker's persistent context
|
|
28
28
|
(`persist: true`), the configured timeout/viewport/captcha/recording policy,
|
|
29
29
|
and `userMetadata` tagging for observability and reaping.
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
- On downstream disconnect the broker **lingers**: the session and its
|
|
31
|
+
upstream connection are held (default 60s) so the next browser action
|
|
32
|
+
re-attaches instantly — no new mint, no cooldown. OpenClaw's browser tool
|
|
33
|
+
opens short probe connections and reconnects between actions; without
|
|
34
|
+
linger every probe minted (and released) a billable session and the
|
|
35
|
+
post-release cooldown starved the real connection. Explicit release, idle
|
|
36
|
+
disconnect, linger expiry, and gateway shutdown all still release
|
|
37
|
+
deterministically; login state survives in the context either way.
|
|
33
38
|
- One session per coworker context at a time (Browserbase forbids concurrent
|
|
34
39
|
sessions on one context), with a short cooldown after release.
|
|
35
40
|
|
|
@@ -90,8 +95,8 @@ Notes:
|
|
|
90
95
|
See `openclaw.plugin.json` for the full schema. Defaults implement the locked
|
|
91
96
|
v1 policy: timeout 3600 s, viewport 1280×900, captcha solving on, recording on,
|
|
92
97
|
`ignoreCertificateErrors: false`, per-coworker context with `persist: true`,
|
|
93
|
-
cooldown
|
|
94
|
-
(`proxies: []` passes through to session creation when set).
|
|
98
|
+
linger 60 s, cooldown 3 s, idle disconnect 15 min, reaper every 5 min, no
|
|
99
|
+
proxies (`proxies: []` passes through to session creation when set).
|
|
95
100
|
|
|
96
101
|
## Agent tools
|
|
97
102
|
|
package/dist/index.js
CHANGED
|
@@ -121,8 +121,18 @@ var BrowserbaseClient = class {
|
|
|
121
121
|
|
|
122
122
|
// src/cdp-pipe.ts
|
|
123
123
|
var INJECTED_ID_BASE = 19e8;
|
|
124
|
+
var INJECTED_ID_PIPE_STRIDE = 1e6;
|
|
125
|
+
var INJECTED_ID_PIPE_SLOTS = 200;
|
|
124
126
|
var INJECTED_RESPONSE_MAX_BYTES = 4096;
|
|
125
127
|
var DEFAULT_INJECTION_TIMEOUT_MS = 2e3;
|
|
128
|
+
var STALE_REPLY_TTL_MS = 15e3;
|
|
129
|
+
var MAX_TRACKED_CLIENT_IDS = 256;
|
|
130
|
+
var FRAME_HEAD_ID_RE = /"id"\s*:\s*(\d+)/;
|
|
131
|
+
function frameHeadId(data) {
|
|
132
|
+
const head = Buffer.isBuffer(data) ? data.subarray(0, 64).toString("utf8") : Array.isArray(data) ? Buffer.concat(data).subarray(0, 64).toString("utf8") : Buffer.from(data).subarray(0, 64).toString("utf8");
|
|
133
|
+
const match = FRAME_HEAD_ID_RE.exec(head);
|
|
134
|
+
return match ? Number(match[1]) : null;
|
|
135
|
+
}
|
|
126
136
|
function rawDataByteLength(data) {
|
|
127
137
|
if (Buffer.isBuffer(data)) {
|
|
128
138
|
return data.byteLength;
|
|
@@ -141,38 +151,90 @@ function rawDataToString(data) {
|
|
|
141
151
|
}
|
|
142
152
|
return Buffer.from(data).toString("utf8");
|
|
143
153
|
}
|
|
144
|
-
var CdpPipe = class {
|
|
154
|
+
var CdpPipe = class _CdpPipe {
|
|
145
155
|
constructor(downstream, upstream, opts) {
|
|
146
156
|
this.downstream = downstream;
|
|
147
157
|
this.upstream = upstream;
|
|
148
158
|
this.opts = opts;
|
|
159
|
+
const ttl = Date.now() + STALE_REPLY_TTL_MS;
|
|
160
|
+
for (const id of opts.suppressStaleIds ?? []) {
|
|
161
|
+
this.suppressStale.set(id, ttl);
|
|
162
|
+
}
|
|
149
163
|
downstream.on("message", (data, isBinary) => {
|
|
150
164
|
this.lastActivityAt = Date.now();
|
|
165
|
+
if (!isBinary && this.clientPendingIds.size < MAX_TRACKED_CLIENT_IDS) {
|
|
166
|
+
const id = frameHeadId(data);
|
|
167
|
+
if (id !== null && id < INJECTED_ID_BASE) {
|
|
168
|
+
this.clientPendingIds.add(id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
151
171
|
if (upstream.readyState === upstream.OPEN) {
|
|
152
172
|
upstream.send(data, { binary: isBinary });
|
|
153
173
|
}
|
|
154
174
|
});
|
|
155
|
-
upstream.on("message",
|
|
156
|
-
this.lastActivityAt = Date.now();
|
|
157
|
-
if (!isBinary && this.maybeResolveInjected(data)) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (downstream.readyState === downstream.OPEN) {
|
|
161
|
-
downstream.send(data, { binary: isBinary });
|
|
162
|
-
}
|
|
163
|
-
});
|
|
175
|
+
upstream.on("message", this.onUpstreamMessage);
|
|
164
176
|
downstream.on("close", () => this.close("downstream-closed"));
|
|
165
177
|
downstream.on("error", () => this.close("downstream-error"));
|
|
166
|
-
upstream.on("close",
|
|
167
|
-
upstream.on("error",
|
|
178
|
+
upstream.on("close", this.onUpstreamClose);
|
|
179
|
+
upstream.on("error", this.onUpstreamError);
|
|
168
180
|
}
|
|
169
|
-
|
|
181
|
+
static pipeSeq = 0;
|
|
182
|
+
nextInjectedId = INJECTED_ID_BASE + _CdpPipe.pipeSeq++ % INJECTED_ID_PIPE_SLOTS * INJECTED_ID_PIPE_STRIDE;
|
|
170
183
|
pending = /* @__PURE__ */ new Map();
|
|
171
184
|
closeReason = null;
|
|
185
|
+
upstreamClaimed = false;
|
|
186
|
+
/** Downstream command ids sent upstream with no reply seen yet. */
|
|
187
|
+
clientPendingIds = /* @__PURE__ */ new Set();
|
|
188
|
+
/** Stale ids from the previous client on this upstream → suppression deadline. */
|
|
189
|
+
suppressStale = /* @__PURE__ */ new Map();
|
|
172
190
|
lastActivityAt = Date.now();
|
|
191
|
+
// Named so takeUpstream() can detach exactly the pipe's listeners from a
|
|
192
|
+
// claimed (lingering) upstream without disturbing the hold's own listeners.
|
|
193
|
+
onUpstreamMessage = (data, isBinary) => {
|
|
194
|
+
this.lastActivityAt = Date.now();
|
|
195
|
+
if (!isBinary) {
|
|
196
|
+
const id = frameHeadId(data);
|
|
197
|
+
if (id !== null) {
|
|
198
|
+
this.clientPendingIds.delete(id);
|
|
199
|
+
const deadline = this.suppressStale.get(id);
|
|
200
|
+
if (deadline !== void 0) {
|
|
201
|
+
this.suppressStale.delete(id);
|
|
202
|
+
if (Date.now() <= deadline) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (this.maybeResolveInjected(data)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (this.downstream.readyState === this.downstream.OPEN) {
|
|
212
|
+
this.downstream.send(data, { binary: isBinary });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
onUpstreamClose = () => this.close("upstream-closed");
|
|
216
|
+
onUpstreamError = () => this.close("upstream-error");
|
|
173
217
|
get closed() {
|
|
174
218
|
return this.closeReason !== null;
|
|
175
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Claim the upstream socket out of the pipe while it is still OPEN —
|
|
222
|
+
* callable from inside the onClose handler so the broker can keep the
|
|
223
|
+
* Browserbase session alive (linger) instead of releasing on downstream
|
|
224
|
+
* disconnect. Returns null when the upstream is already gone or claimed.
|
|
225
|
+
* staleClientIds are the old client's still-unanswered command ids; the
|
|
226
|
+
* next pipe on this upstream must suppress their late replies.
|
|
227
|
+
*/
|
|
228
|
+
takeUpstream() {
|
|
229
|
+
if (this.upstreamClaimed || this.upstream.readyState !== this.upstream.OPEN) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
this.upstreamClaimed = true;
|
|
233
|
+
this.upstream.off("message", this.onUpstreamMessage);
|
|
234
|
+
this.upstream.off("close", this.onUpstreamClose);
|
|
235
|
+
this.upstream.off("error", this.onUpstreamError);
|
|
236
|
+
return { socket: this.upstream, staleClientIds: [...this.clientPendingIds] };
|
|
237
|
+
}
|
|
176
238
|
/** Send a broker-owned CDP command on the piped connection and await its reply. */
|
|
177
239
|
inject(method, params) {
|
|
178
240
|
if (this.closeReason !== null || this.upstream.readyState !== this.upstream.OPEN) {
|
|
@@ -228,7 +290,9 @@ var CdpPipe = class {
|
|
|
228
290
|
entry.reject(new Error(`CDP pipe closed: ${reason}`));
|
|
229
291
|
}
|
|
230
292
|
this.pending.clear();
|
|
231
|
-
|
|
293
|
+
this.opts.onClose(reason);
|
|
294
|
+
const sockets = this.upstreamClaimed ? [this.downstream] : [this.downstream, this.upstream];
|
|
295
|
+
for (const socket of sockets) {
|
|
232
296
|
if (socket.readyState === socket.OPEN || socket.readyState === socket.CLOSING) {
|
|
233
297
|
try {
|
|
234
298
|
socket.close(1e3);
|
|
@@ -239,7 +303,6 @@ var CdpPipe = class {
|
|
|
239
303
|
socket.terminate();
|
|
240
304
|
}
|
|
241
305
|
}
|
|
242
|
-
this.opts.onClose(reason);
|
|
243
306
|
}
|
|
244
307
|
};
|
|
245
308
|
|
|
@@ -250,8 +313,9 @@ var DEFAULT_TIMEOUT_SECONDS = 3600;
|
|
|
250
313
|
var MIN_TIMEOUT_SECONDS = 60;
|
|
251
314
|
var MAX_TIMEOUT_SECONDS = 21600;
|
|
252
315
|
var DEFAULT_VIEWPORT = { width: 1280, height: 900 };
|
|
253
|
-
var DEFAULT_CONTEXT_COOLDOWN_SECONDS =
|
|
316
|
+
var DEFAULT_CONTEXT_COOLDOWN_SECONDS = 3;
|
|
254
317
|
var DEFAULT_CONTEXT_WAIT_MS = 1e4;
|
|
318
|
+
var DEFAULT_LINGER_SECONDS = 60;
|
|
255
319
|
var DEFAULT_IDLE_DISCONNECT_MINUTES = 15;
|
|
256
320
|
var DEFAULT_REAPER_INTERVAL_MINUTES = 5;
|
|
257
321
|
var DEFAULT_API_BASE_URL = "https://api.browserbase.com";
|
|
@@ -368,6 +432,7 @@ function normalizeConfig(raw) {
|
|
|
368
432
|
waitMs: readNumber(context.waitMs, DEFAULT_CONTEXT_WAIT_MS, 0, 6e4)
|
|
369
433
|
},
|
|
370
434
|
proxies: readProxies(root.proxies),
|
|
435
|
+
lingerSeconds: readNumber(root.lingerSeconds, DEFAULT_LINGER_SECONDS, 0, 600),
|
|
371
436
|
idleDisconnectMinutes: readNumber(root.idleDisconnectMinutes, DEFAULT_IDLE_DISCONNECT_MINUTES, 0, 1440),
|
|
372
437
|
reaperIntervalMinutes: readNumber(root.reaperIntervalMinutes, DEFAULT_REAPER_INTERVAL_MINUTES, 0, 1440),
|
|
373
438
|
apiBaseUrl: readString(root.apiBaseUrl) ?? DEFAULT_API_BASE_URL,
|
|
@@ -752,7 +817,7 @@ var BrowserbaseBroker = class {
|
|
|
752
817
|
if (liveness.gone) {
|
|
753
818
|
liveness.detach();
|
|
754
819
|
socket.destroy();
|
|
755
|
-
this.holdForAdoption(minted, held.upstream);
|
|
820
|
+
this.holdForAdoption(minted, held.upstream, { staleClientIds: held.staleClientIds ?? [] });
|
|
756
821
|
return;
|
|
757
822
|
}
|
|
758
823
|
let upstream = held.upstream && held.upstream.readyState === held.upstream.OPEN ? held.upstream : null;
|
|
@@ -777,15 +842,16 @@ var BrowserbaseBroker = class {
|
|
|
777
842
|
if (liveness.gone) {
|
|
778
843
|
liveness.detach();
|
|
779
844
|
socket.destroy();
|
|
780
|
-
this.holdForAdoption(minted, upstream);
|
|
845
|
+
this.holdForAdoption(minted, upstream, { staleClientIds: held.staleClientIds ?? [] });
|
|
781
846
|
return;
|
|
782
847
|
}
|
|
783
848
|
liveness.detach();
|
|
849
|
+
const staleClientIds = held.staleClientIds ?? [];
|
|
784
850
|
this.wss.handleUpgrade(req, socket, head, (downstream) => {
|
|
785
|
-
this.attachPipe(downstream, upstream, minted);
|
|
851
|
+
this.attachPipe(downstream, upstream, minted, staleClientIds);
|
|
786
852
|
});
|
|
787
853
|
}
|
|
788
|
-
attachPipe(downstream, upstream, minted) {
|
|
854
|
+
attachPipe(downstream, upstream, minted, staleClientIds = []) {
|
|
789
855
|
if (upstream.readyState !== upstream.OPEN || downstream.readyState !== downstream.OPEN) {
|
|
790
856
|
upstream.terminate();
|
|
791
857
|
if (downstream.readyState === downstream.OPEN) {
|
|
@@ -798,12 +864,14 @@ var BrowserbaseBroker = class {
|
|
|
798
864
|
return;
|
|
799
865
|
}
|
|
800
866
|
const pipe = new CdpPipe(downstream, upstream, {
|
|
801
|
-
onClose: (reason) => this.onPipeClosed(minted.sessionId, reason)
|
|
867
|
+
onClose: (reason) => this.onPipeClosed(minted.sessionId, reason),
|
|
868
|
+
...staleClientIds.length ? { suppressStaleIds: staleClientIds } : {}
|
|
802
869
|
});
|
|
803
870
|
this.active = {
|
|
804
871
|
sessionId: minted.sessionId,
|
|
805
872
|
leaseId: minted.leaseId,
|
|
806
873
|
contextId: minted.contextId,
|
|
874
|
+
connectUrl: minted.connectUrl,
|
|
807
875
|
...minted.expiresAt ? { expiresAt: minted.expiresAt } : {},
|
|
808
876
|
startedAt: Date.now(),
|
|
809
877
|
pipe
|
|
@@ -813,10 +881,29 @@ var BrowserbaseBroker = class {
|
|
|
813
881
|
);
|
|
814
882
|
}
|
|
815
883
|
onPipeClosed(sessionId, reason) {
|
|
816
|
-
|
|
884
|
+
const active = this.active;
|
|
885
|
+
if (active?.sessionId !== sessionId) {
|
|
817
886
|
return;
|
|
818
887
|
}
|
|
819
888
|
this.active = null;
|
|
889
|
+
const downstreamGone = reason === "downstream-closed" || reason === "downstream-error";
|
|
890
|
+
if (downstreamGone && !this.stopping && this.config.lingerSeconds > 0) {
|
|
891
|
+
const claimed = active.pipe.takeUpstream();
|
|
892
|
+
if (claimed) {
|
|
893
|
+
this.holdForAdoption(
|
|
894
|
+
{
|
|
895
|
+
sessionId: active.sessionId,
|
|
896
|
+
connectUrl: active.connectUrl,
|
|
897
|
+
contextId: active.contextId,
|
|
898
|
+
leaseId: active.leaseId,
|
|
899
|
+
...active.expiresAt ? { expiresAt: active.expiresAt } : {}
|
|
900
|
+
},
|
|
901
|
+
claimed.socket,
|
|
902
|
+
{ windowMs: this.config.lingerSeconds * 1e3, reason: "linger", staleClientIds: claimed.staleClientIds }
|
|
903
|
+
);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
820
907
|
this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
|
|
821
908
|
this.logger.info?.(`session ${sessionId} disconnected (${reason}); releasing`);
|
|
822
909
|
void this.releaseQuietly(sessionId, reason).finally(() => {
|
|
@@ -907,18 +994,21 @@ var BrowserbaseBroker = class {
|
|
|
907
994
|
});
|
|
908
995
|
});
|
|
909
996
|
}
|
|
910
|
-
holdForAdoption(session, upstream) {
|
|
997
|
+
holdForAdoption(session, upstream, opts = {}) {
|
|
998
|
+
const reason = opts.reason ?? "adoption";
|
|
999
|
+
const windowMs = opts.windowMs ?? ADOPTION_WINDOW_MS;
|
|
911
1000
|
this.logger.info?.(
|
|
912
|
-
`holding session ${session.sessionId}
|
|
1001
|
+
`holding session ${session.sessionId} (${reason}, ${Math.round(windowMs / 1e3)}s window, upstream ${upstream ? "open" : "not dialed"})`
|
|
913
1002
|
);
|
|
914
1003
|
const timer = setTimeout(() => {
|
|
915
1004
|
if (this.adoptable?.session.sessionId === session.sessionId) {
|
|
916
1005
|
const held = this.adoptable;
|
|
917
1006
|
this.adoptable = null;
|
|
918
1007
|
held.upstream?.terminate();
|
|
919
|
-
|
|
1008
|
+
this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
|
|
1009
|
+
void this.releaseQuietly(session.sessionId, `${reason}-expired`);
|
|
920
1010
|
}
|
|
921
|
-
},
|
|
1011
|
+
}, windowMs);
|
|
922
1012
|
timer.unref();
|
|
923
1013
|
let onUpstreamLost;
|
|
924
1014
|
if (upstream) {
|
|
@@ -926,7 +1016,8 @@ var BrowserbaseBroker = class {
|
|
|
926
1016
|
if (this.adoptable?.session.sessionId === session.sessionId && this.adoptable.upstream === upstream) {
|
|
927
1017
|
clearTimeout(this.adoptable.timer);
|
|
928
1018
|
this.adoptable = null;
|
|
929
|
-
|
|
1019
|
+
this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
|
|
1020
|
+
void this.releaseQuietly(session.sessionId, `${reason}-upstream-lost`);
|
|
930
1021
|
}
|
|
931
1022
|
};
|
|
932
1023
|
upstream.once("close", onUpstreamLost);
|
|
@@ -934,7 +1025,9 @@ var BrowserbaseBroker = class {
|
|
|
934
1025
|
}
|
|
935
1026
|
this.adoptable = {
|
|
936
1027
|
session,
|
|
1028
|
+
heldReason: reason,
|
|
937
1029
|
...upstream ? { upstream } : {},
|
|
1030
|
+
...opts.staleClientIds?.length ? { staleClientIds: opts.staleClientIds } : {},
|
|
938
1031
|
...onUpstreamLost ? { onUpstreamLost } : {},
|
|
939
1032
|
timer
|
|
940
1033
|
};
|
|
@@ -952,7 +1045,8 @@ var BrowserbaseBroker = class {
|
|
|
952
1045
|
this.adoptable = null;
|
|
953
1046
|
return {
|
|
954
1047
|
session: held.session,
|
|
955
|
-
...held.upstream ? { upstream: held.upstream } : {}
|
|
1048
|
+
...held.upstream ? { upstream: held.upstream } : {},
|
|
1049
|
+
...held.staleClientIds?.length ? { staleClientIds: held.staleClientIds } : {}
|
|
956
1050
|
};
|
|
957
1051
|
}
|
|
958
1052
|
// ------------------------------------------------------------- cleanup --
|
|
@@ -1019,24 +1113,32 @@ var BrowserbaseBroker = class {
|
|
|
1019
1113
|
startedAt: new Date(active.startedAt).toISOString(),
|
|
1020
1114
|
...active.expiresAt ? { expiresAt: active.expiresAt } : {},
|
|
1021
1115
|
idleMs: Date.now() - active.pipe.lastActivityAt
|
|
1022
|
-
} : null
|
|
1116
|
+
} : null,
|
|
1117
|
+
held: this.adoptable ? { sessionId: this.adoptable.session.sessionId, reason: this.adoptable.heldReason } : null
|
|
1023
1118
|
};
|
|
1024
1119
|
}
|
|
1025
1120
|
async getLiveViewUrls() {
|
|
1026
|
-
const
|
|
1027
|
-
if (!
|
|
1121
|
+
const sessionId = this.active && !this.active.pipe.closed ? this.active.sessionId : this.adoptable?.upstream ? this.adoptable.session.sessionId : null;
|
|
1122
|
+
if (!sessionId) {
|
|
1028
1123
|
return null;
|
|
1029
1124
|
}
|
|
1030
|
-
return this.client.getDebugUrls(
|
|
1125
|
+
return this.client.getDebugUrls(sessionId);
|
|
1031
1126
|
}
|
|
1032
1127
|
/** Returns true when there was an active session to release. */
|
|
1033
1128
|
releaseActiveSession() {
|
|
1034
1129
|
const active = this.active;
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1130
|
+
if (active && !active.pipe.closed) {
|
|
1131
|
+
active.pipe.close("manual-release");
|
|
1132
|
+
return true;
|
|
1037
1133
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1134
|
+
const held = this.takeAdoptable();
|
|
1135
|
+
if (held) {
|
|
1136
|
+
held.upstream?.terminate();
|
|
1137
|
+
this.cooldownUntil = Date.now() + this.config.context.cooldownSeconds * 1e3;
|
|
1138
|
+
void this.releaseQuietly(held.session.sessionId, "manual-release");
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
return false;
|
|
1040
1142
|
}
|
|
1041
1143
|
};
|
|
1042
1144
|
|
|
@@ -1063,7 +1165,7 @@ function createBrowserbaseTools(getBroker) {
|
|
|
1063
1165
|
const status = broker.getStatus();
|
|
1064
1166
|
if (!status.active) {
|
|
1065
1167
|
const lines2 = [
|
|
1066
|
-
NO_SESSION,
|
|
1168
|
+
status.held?.reason === "linger" ? `Browserbase session ${status.held.sessionId} is lingering for instant resume \u2014 the next browser action re-attaches to it (same tabs may be gone, login state intact).` : NO_SESSION,
|
|
1067
1169
|
`Region: ${status.region}. Recording: ${status.recordSession ? "on" : "off"}.`,
|
|
1068
1170
|
status.contextId ? `Browser context: ${status.contextId}` : "Browser context: created on first use."
|
|
1069
1171
|
];
|
package/openclaw.plugin.json
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"configSchema": {
|
|
9
9
|
"type": "object",
|
|
10
10
|
"additionalProperties": false,
|
|
11
|
-
"required": ["projectId", "apiKey", "listenPort"],
|
|
12
11
|
"properties": {
|
|
13
12
|
"projectId": {
|
|
14
13
|
"type": "string",
|
|
@@ -20,7 +19,12 @@
|
|
|
20
19
|
},
|
|
21
20
|
"region": {
|
|
22
21
|
"type": "string",
|
|
23
|
-
"enum": [
|
|
22
|
+
"enum": [
|
|
23
|
+
"us-west-2",
|
|
24
|
+
"us-east-1",
|
|
25
|
+
"eu-central-1",
|
|
26
|
+
"ap-southeast-1"
|
|
27
|
+
],
|
|
24
28
|
"default": "us-west-2"
|
|
25
29
|
},
|
|
26
30
|
"listenPort": {
|
|
@@ -38,7 +42,9 @@
|
|
|
38
42
|
},
|
|
39
43
|
"metadata": {
|
|
40
44
|
"type": "object",
|
|
41
|
-
"additionalProperties": {
|
|
45
|
+
"additionalProperties": {
|
|
46
|
+
"type": "string"
|
|
47
|
+
}
|
|
42
48
|
},
|
|
43
49
|
"defaults": {
|
|
44
50
|
"type": "object",
|
|
@@ -54,28 +60,63 @@
|
|
|
54
60
|
"type": "object",
|
|
55
61
|
"additionalProperties": false,
|
|
56
62
|
"properties": {
|
|
57
|
-
"width": {
|
|
58
|
-
|
|
63
|
+
"width": {
|
|
64
|
+
"type": "number",
|
|
65
|
+
"minimum": 320,
|
|
66
|
+
"maximum": 3840
|
|
67
|
+
},
|
|
68
|
+
"height": {
|
|
69
|
+
"type": "number",
|
|
70
|
+
"minimum": 320,
|
|
71
|
+
"maximum": 2160
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
74
|
},
|
|
61
|
-
"solveCaptchas": {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
"solveCaptchas": {
|
|
76
|
+
"type": "boolean",
|
|
77
|
+
"default": true
|
|
78
|
+
},
|
|
79
|
+
"recordSession": {
|
|
80
|
+
"type": "boolean",
|
|
81
|
+
"default": true
|
|
82
|
+
},
|
|
83
|
+
"logSession": {
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"default": true
|
|
86
|
+
},
|
|
87
|
+
"ignoreCertificateErrors": {
|
|
88
|
+
"type": "boolean",
|
|
89
|
+
"default": false
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
},
|
|
67
93
|
"context": {
|
|
68
94
|
"type": "object",
|
|
69
95
|
"additionalProperties": false,
|
|
70
96
|
"properties": {
|
|
71
|
-
"persist": {
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
"persist": {
|
|
98
|
+
"type": "boolean",
|
|
99
|
+
"default": true
|
|
100
|
+
},
|
|
101
|
+
"cooldownSeconds": {
|
|
102
|
+
"type": "number",
|
|
103
|
+
"default": 3,
|
|
104
|
+
"minimum": 0,
|
|
105
|
+
"maximum": 120
|
|
106
|
+
},
|
|
107
|
+
"waitMs": {
|
|
108
|
+
"type": "number",
|
|
109
|
+
"default": 10000,
|
|
110
|
+
"minimum": 0,
|
|
111
|
+
"maximum": 60000
|
|
112
|
+
}
|
|
74
113
|
}
|
|
75
114
|
},
|
|
76
115
|
"proxies": {
|
|
77
116
|
"type": "array",
|
|
78
|
-
"items": {
|
|
117
|
+
"items": {
|
|
118
|
+
"type": "object"
|
|
119
|
+
},
|
|
79
120
|
"default": []
|
|
80
121
|
},
|
|
81
122
|
"idleDisconnectMinutes": {
|
|
@@ -87,6 +128,12 @@
|
|
|
87
128
|
"type": "number",
|
|
88
129
|
"default": 5,
|
|
89
130
|
"minimum": 0
|
|
131
|
+
},
|
|
132
|
+
"lingerSeconds": {
|
|
133
|
+
"type": "number",
|
|
134
|
+
"default": 60,
|
|
135
|
+
"minimum": 0,
|
|
136
|
+
"maximum": 600
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
},
|
|
@@ -120,6 +167,10 @@
|
|
|
120
167
|
"idleDisconnectMinutes": {
|
|
121
168
|
"label": "Idle Disconnect (min)",
|
|
122
169
|
"help": "Close and release the Browserbase session after this many minutes without CDP traffic. 0 disables. Login state survives via the persistent context."
|
|
170
|
+
},
|
|
171
|
+
"lingerSeconds": {
|
|
172
|
+
"label": "Linger (s)",
|
|
173
|
+
"help": "Keep the session and its connection alive after a disconnect so the next browser action re-attaches instantly instead of minting a new session. 0 releases immediately."
|
|
123
174
|
}
|
|
124
175
|
}
|
|
125
176
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velanir/openclaw-browserbase",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenClaw plugin that brokers Browserbase sessions behind a loopback CDP endpoint: explicit session creation with per-coworker persistent contexts, deterministic release, and stale-session reaping.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|