@sovovs/bycli 2.1.40 → 2.1.42
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/cli-manifest.json +10 -2
- package/clis/weixin/_wechat/article-artifact.js +55 -0
- package/clis/weixin/_wechat/article-identity.js +27 -0
- package/clis/weixin/_wechat/publish-analysis.js +8 -4
- package/clis/weixin/_wechat/publish-download.js +3 -1
- package/clis/weixin/download-publish-data.js +20 -0
- package/clis/weixin/download.js +15 -2
- package/dist/src/adapter-coordination.d.ts +26 -0
- package/dist/src/adapter-coordination.js +183 -0
- package/dist/src/adapter-coordination.test.d.ts +1 -0
- package/dist/src/adapter-execution-context.d.ts +6 -0
- package/dist/src/adapter-execution-context.js +8 -0
- package/dist/src/adapter-scheduler.d.ts +86 -0
- package/dist/src/adapter-scheduler.js +349 -0
- package/dist/src/adapter-scheduler.test.d.ts +1 -0
- package/dist/src/browser/daemon-client.d.ts +11 -0
- package/dist/src/browser/daemon-client.js +53 -1
- package/dist/src/browser/extension-capabilities.d.ts +1 -0
- package/dist/src/browser/extension-capabilities.js +18 -5
- package/dist/src/browser/page.d.ts +2 -1
- package/dist/src/browser/page.js +3 -0
- package/dist/src/build-manifest.js +1 -0
- package/dist/src/cli-argv-preprocess.d.ts +3 -0
- package/dist/src/cli-argv-preprocess.js +4 -0
- package/dist/src/commanderAdapter.js +11 -0
- package/dist/src/daemon.js +118 -0
- package/dist/src/discovery.js +1 -0
- package/dist/src/download/article-download.d.ts +2 -0
- package/dist/src/download/article-download.js +30 -4
- package/dist/src/errors.d.ts +3 -0
- package/dist/src/errors.js +5 -0
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +172 -105
- package/dist/src/help.d.ts +1 -0
- package/dist/src/help.js +40 -0
- package/dist/src/manifest-types.d.ts +4 -0
- package/dist/src/registry.d.ts +6 -0
- package/dist/src/registry.js +23 -0
- package/dist/src/serialization.d.ts +1 -0
- package/dist/src/serialization.js +1 -0
- package/dist/src/types.d.ts +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
export class AdapterSchedulerError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'AdapterSchedulerError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class AdapterScheduler {
|
|
11
|
+
now;
|
|
12
|
+
leaseExpiryMs;
|
|
13
|
+
runtimeCeiling;
|
|
14
|
+
releasedLeaseRetentionMs;
|
|
15
|
+
pools = new Map();
|
|
16
|
+
releasedLeaseIds = new Map();
|
|
17
|
+
generations = new Map();
|
|
18
|
+
resourceOwners = new Map();
|
|
19
|
+
resourceGrants = new Map();
|
|
20
|
+
resourceQueue = [];
|
|
21
|
+
sequence = 0;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.now = options.now ?? Date.now;
|
|
24
|
+
this.leaseExpiryMs = options.leaseExpiryMs ?? 45_000;
|
|
25
|
+
this.runtimeCeiling = options.runtimeCeiling ?? 3;
|
|
26
|
+
this.releasedLeaseRetentionMs = options.releasedLeaseRetentionMs ?? 300_000;
|
|
27
|
+
}
|
|
28
|
+
acquire(request) {
|
|
29
|
+
this.validateRequest(request);
|
|
30
|
+
const pool = this.getOrCreatePool(request);
|
|
31
|
+
if (pool.closed) {
|
|
32
|
+
return Promise.reject(this.poolClosedError(pool.closed));
|
|
33
|
+
}
|
|
34
|
+
pool.maxParallel = Math.min(pool.maxParallel, request.maxParallel, this.runtimeCeiling);
|
|
35
|
+
if ([...pool.running.values()].some(lease => lease.requestId === request.requestId)
|
|
36
|
+
|| pool.queued.some(entry => entry.request.requestId === request.requestId)) {
|
|
37
|
+
return Promise.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Duplicate Adapter lease request id'));
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const enqueuedAt = this.now();
|
|
41
|
+
const pending = {
|
|
42
|
+
request,
|
|
43
|
+
enqueuedAt,
|
|
44
|
+
deadline: enqueuedAt + request.queueTimeoutMs,
|
|
45
|
+
sequence: ++this.sequence,
|
|
46
|
+
resolve,
|
|
47
|
+
reject,
|
|
48
|
+
};
|
|
49
|
+
pending.timer = setTimeout(() => this.sweepExpired(), request.queueTimeoutMs);
|
|
50
|
+
pending.timer.unref?.();
|
|
51
|
+
pool.queued.push(pending);
|
|
52
|
+
this.schedule(pool);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
heartbeat(identity) {
|
|
56
|
+
const lease = this.requireLease(identity);
|
|
57
|
+
lease.heartbeatDeadline = this.now() + this.leaseExpiryMs;
|
|
58
|
+
return { ...lease };
|
|
59
|
+
}
|
|
60
|
+
assertLease(identity) {
|
|
61
|
+
return { ...this.requireLease(identity) };
|
|
62
|
+
}
|
|
63
|
+
release(release) {
|
|
64
|
+
this.pruneReleasedLeaseIds();
|
|
65
|
+
if (this.releasedLeaseIds.has(release.leaseId))
|
|
66
|
+
return false;
|
|
67
|
+
const lease = this.requireLease(release);
|
|
68
|
+
const pool = this.pools.get(lease.poolKey);
|
|
69
|
+
this.releaseAllResourcesForLease(lease.leaseId);
|
|
70
|
+
this.rejectResourceWaitersForLease(lease.leaseId);
|
|
71
|
+
pool.running.delete(lease.leaseId);
|
|
72
|
+
pool.activeSessions.delete(lease.adapterSession);
|
|
73
|
+
this.releasedLeaseIds.set(lease.leaseId, this.now() + this.releasedLeaseRetentionMs);
|
|
74
|
+
if (release.reason === 'auth_gate' || release.reason === 'rate_limited') {
|
|
75
|
+
pool.closed = release.reason;
|
|
76
|
+
const error = this.poolClosedError(release.reason);
|
|
77
|
+
for (const pending of pool.queued.splice(0)) {
|
|
78
|
+
if (pending.timer)
|
|
79
|
+
clearTimeout(pending.timer);
|
|
80
|
+
pending.reject(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.schedule(pool);
|
|
85
|
+
}
|
|
86
|
+
this.scheduleResources();
|
|
87
|
+
this.removeDrainedPool(pool);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
acquireResources(leaseIdentity, rawKeys, timeoutMs) {
|
|
91
|
+
const lease = this.requireLease(leaseIdentity);
|
|
92
|
+
const keys = [...new Set(rawKeys.map(key => key.trim()).filter(Boolean))].sort();
|
|
93
|
+
if (keys.length === 0) {
|
|
94
|
+
return Promise.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'At least one resource key is required'));
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
|
|
97
|
+
return Promise.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'Invalid Adapter resource timeout'));
|
|
98
|
+
}
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const pending = {
|
|
101
|
+
lease: { ...lease },
|
|
102
|
+
keys,
|
|
103
|
+
deadline: this.now() + timeoutMs,
|
|
104
|
+
sequence: ++this.sequence,
|
|
105
|
+
resolve,
|
|
106
|
+
reject,
|
|
107
|
+
};
|
|
108
|
+
pending.timer = setTimeout(() => this.sweepExpired(), timeoutMs);
|
|
109
|
+
pending.timer.unref?.();
|
|
110
|
+
this.resourceQueue.push(pending);
|
|
111
|
+
this.scheduleResources();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
releaseResources(leaseIdentity, grantId) {
|
|
115
|
+
const lease = this.requireLease(leaseIdentity);
|
|
116
|
+
const grant = this.resourceGrants.get(grantId);
|
|
117
|
+
if (!grant || grant.leaseId !== lease.leaseId) {
|
|
118
|
+
throw new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter resource grant is no longer valid');
|
|
119
|
+
}
|
|
120
|
+
this.resourceGrants.delete(grantId);
|
|
121
|
+
for (const key of grant.keys) {
|
|
122
|
+
const owner = this.resourceOwners.get(key);
|
|
123
|
+
if (owner?.grantId === grantId)
|
|
124
|
+
this.resourceOwners.delete(key);
|
|
125
|
+
}
|
|
126
|
+
this.scheduleResources();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
cancel(requestId, code = 'ADAPTER_QUEUE_RESET') {
|
|
130
|
+
for (const pool of this.pools.values()) {
|
|
131
|
+
const index = pool.queued.findIndex(entry => entry.request.requestId === requestId);
|
|
132
|
+
if (index === -1)
|
|
133
|
+
continue;
|
|
134
|
+
const [pending] = pool.queued.splice(index, 1);
|
|
135
|
+
if (pending.timer)
|
|
136
|
+
clearTimeout(pending.timer);
|
|
137
|
+
pending.reject(new AdapterSchedulerError(code, 'Adapter lease request was cancelled'));
|
|
138
|
+
this.removeDrainedPool(pool);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
sweepExpired() {
|
|
144
|
+
const now = this.now();
|
|
145
|
+
this.pruneReleasedLeaseIds(now);
|
|
146
|
+
for (const pool of [...this.pools.values()]) {
|
|
147
|
+
for (const pending of [...pool.queued]) {
|
|
148
|
+
if (pending.deadline > now)
|
|
149
|
+
continue;
|
|
150
|
+
pool.queued.splice(pool.queued.indexOf(pending), 1);
|
|
151
|
+
if (pending.timer)
|
|
152
|
+
clearTimeout(pending.timer);
|
|
153
|
+
pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_TIMEOUT', 'Timed out waiting for an Adapter command lease'));
|
|
154
|
+
}
|
|
155
|
+
for (const lease of [...pool.running.values()]) {
|
|
156
|
+
if (lease.heartbeatDeadline > now)
|
|
157
|
+
continue;
|
|
158
|
+
this.releaseAllResourcesForLease(lease.leaseId);
|
|
159
|
+
this.rejectResourceWaitersForLease(lease.leaseId);
|
|
160
|
+
pool.running.delete(lease.leaseId);
|
|
161
|
+
pool.activeSessions.delete(lease.adapterSession);
|
|
162
|
+
}
|
|
163
|
+
if (!pool.closed)
|
|
164
|
+
this.schedule(pool);
|
|
165
|
+
this.removeDrainedPool(pool);
|
|
166
|
+
}
|
|
167
|
+
for (const pending of [...this.resourceQueue]) {
|
|
168
|
+
if (pending.deadline > now)
|
|
169
|
+
continue;
|
|
170
|
+
this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
|
|
171
|
+
if (pending.timer)
|
|
172
|
+
clearTimeout(pending.timer);
|
|
173
|
+
pending.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'Timed out waiting for Adapter resource locks'));
|
|
174
|
+
}
|
|
175
|
+
this.scheduleResources();
|
|
176
|
+
}
|
|
177
|
+
reset() {
|
|
178
|
+
for (const pool of this.pools.values()) {
|
|
179
|
+
for (const pending of pool.queued) {
|
|
180
|
+
if (pending.timer)
|
|
181
|
+
clearTimeout(pending.timer);
|
|
182
|
+
pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Adapter scheduler restarted'));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.pools.clear();
|
|
186
|
+
for (const pending of this.resourceQueue.splice(0)) {
|
|
187
|
+
if (pending.timer)
|
|
188
|
+
clearTimeout(pending.timer);
|
|
189
|
+
pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Adapter scheduler restarted'));
|
|
190
|
+
}
|
|
191
|
+
this.resourceOwners.clear();
|
|
192
|
+
this.resourceGrants.clear();
|
|
193
|
+
this.releasedLeaseIds.clear();
|
|
194
|
+
}
|
|
195
|
+
snapshot() {
|
|
196
|
+
let running = 0;
|
|
197
|
+
let queued = 0;
|
|
198
|
+
for (const pool of this.pools.values()) {
|
|
199
|
+
running += pool.running.size;
|
|
200
|
+
queued += pool.queued.length;
|
|
201
|
+
}
|
|
202
|
+
return { running, queued, pools: this.pools.size };
|
|
203
|
+
}
|
|
204
|
+
resourceSnapshot() {
|
|
205
|
+
return {
|
|
206
|
+
locked: this.resourceOwners.size,
|
|
207
|
+
queued: this.resourceQueue.length,
|
|
208
|
+
grants: this.resourceGrants.size,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
schedule(pool) {
|
|
212
|
+
while (!pool.closed && pool.running.size < pool.maxParallel) {
|
|
213
|
+
const eligible = pool.queued
|
|
214
|
+
.filter(entry => !pool.activeSessions.has(entry.request.adapterSession)
|
|
215
|
+
&& pool.running.size < pool.maxParallel)
|
|
216
|
+
.sort((a, b) => a.enqueuedAt - b.enqueuedAt || a.sequence - b.sequence)[0];
|
|
217
|
+
if (!eligible)
|
|
218
|
+
return;
|
|
219
|
+
pool.queued.splice(pool.queued.indexOf(eligible), 1);
|
|
220
|
+
if (eligible.timer)
|
|
221
|
+
clearTimeout(eligible.timer);
|
|
222
|
+
const grantedAt = this.now();
|
|
223
|
+
const lease = {
|
|
224
|
+
leaseId: crypto.randomUUID(),
|
|
225
|
+
requestId: eligible.request.requestId,
|
|
226
|
+
poolKey: pool.key,
|
|
227
|
+
contextId: eligible.request.contextId,
|
|
228
|
+
surface: 'adapter',
|
|
229
|
+
site: eligible.request.site,
|
|
230
|
+
adapterSession: eligible.request.adapterSession,
|
|
231
|
+
sessionKey: eligible.request.sessionKey,
|
|
232
|
+
generation: pool.generation,
|
|
233
|
+
grantedAt,
|
|
234
|
+
heartbeatDeadline: grantedAt + this.leaseExpiryMs,
|
|
235
|
+
};
|
|
236
|
+
pool.running.set(lease.leaseId, lease);
|
|
237
|
+
pool.activeSessions.add(lease.adapterSession);
|
|
238
|
+
eligible.resolve({ ...lease });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
scheduleResources() {
|
|
242
|
+
for (const pending of [...this.resourceQueue].sort((a, b) => a.sequence - b.sequence)) {
|
|
243
|
+
try {
|
|
244
|
+
this.requireLease(pending.lease);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
|
|
248
|
+
if (pending.timer)
|
|
249
|
+
clearTimeout(pending.timer);
|
|
250
|
+
pending.reject(new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease was lost while waiting for resources'));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!pending.keys.every(key => !this.resourceOwners.has(key)))
|
|
254
|
+
continue;
|
|
255
|
+
this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
|
|
256
|
+
if (pending.timer)
|
|
257
|
+
clearTimeout(pending.timer);
|
|
258
|
+
const grant = {
|
|
259
|
+
grantId: crypto.randomUUID(),
|
|
260
|
+
leaseId: pending.lease.leaseId,
|
|
261
|
+
keys: pending.keys,
|
|
262
|
+
};
|
|
263
|
+
this.resourceGrants.set(grant.grantId, grant);
|
|
264
|
+
for (const key of grant.keys)
|
|
265
|
+
this.resourceOwners.set(key, { leaseId: grant.leaseId, grantId: grant.grantId });
|
|
266
|
+
pending.resolve({ ...grant, keys: [...grant.keys] });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
releaseAllResourcesForLease(leaseId) {
|
|
270
|
+
for (const grant of [...this.resourceGrants.values()]) {
|
|
271
|
+
if (grant.leaseId !== leaseId)
|
|
272
|
+
continue;
|
|
273
|
+
this.resourceGrants.delete(grant.grantId);
|
|
274
|
+
for (const key of grant.keys) {
|
|
275
|
+
const owner = this.resourceOwners.get(key);
|
|
276
|
+
if (owner?.grantId === grant.grantId)
|
|
277
|
+
this.resourceOwners.delete(key);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
pruneReleasedLeaseIds(now = this.now()) {
|
|
282
|
+
for (const [leaseId, expiresAt] of this.releasedLeaseIds) {
|
|
283
|
+
if (expiresAt <= now)
|
|
284
|
+
this.releasedLeaseIds.delete(leaseId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
rejectResourceWaitersForLease(leaseId) {
|
|
288
|
+
for (const pending of [...this.resourceQueue]) {
|
|
289
|
+
if (pending.lease.leaseId !== leaseId)
|
|
290
|
+
continue;
|
|
291
|
+
this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
|
|
292
|
+
if (pending.timer)
|
|
293
|
+
clearTimeout(pending.timer);
|
|
294
|
+
pending.reject(new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease ended while waiting for resources'));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
requireLease(identity) {
|
|
298
|
+
const pool = this.pools.get(identity.poolKey);
|
|
299
|
+
const lease = pool?.running.get(identity.leaseId);
|
|
300
|
+
if (!lease
|
|
301
|
+
|| lease.requestId !== identity.requestId
|
|
302
|
+
|| lease.contextId !== identity.contextId
|
|
303
|
+
|| lease.site !== identity.site
|
|
304
|
+
|| lease.adapterSession !== identity.adapterSession
|
|
305
|
+
|| lease.sessionKey !== identity.sessionKey
|
|
306
|
+
|| lease.generation !== identity.generation) {
|
|
307
|
+
throw new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease is no longer valid');
|
|
308
|
+
}
|
|
309
|
+
return lease;
|
|
310
|
+
}
|
|
311
|
+
getOrCreatePool(request) {
|
|
312
|
+
const key = `${request.contextId}\u0000${request.surface}\u0000${request.site}`;
|
|
313
|
+
const existing = this.pools.get(key);
|
|
314
|
+
if (existing)
|
|
315
|
+
return existing;
|
|
316
|
+
const generation = (this.generations.get(key) ?? 0) + 1;
|
|
317
|
+
this.generations.set(key, generation);
|
|
318
|
+
const pool = {
|
|
319
|
+
key,
|
|
320
|
+
generation,
|
|
321
|
+
maxParallel: Math.min(request.maxParallel, this.runtimeCeiling),
|
|
322
|
+
running: new Map(),
|
|
323
|
+
activeSessions: new Set(),
|
|
324
|
+
queued: [],
|
|
325
|
+
};
|
|
326
|
+
this.pools.set(key, pool);
|
|
327
|
+
return pool;
|
|
328
|
+
}
|
|
329
|
+
removeDrainedPool(pool) {
|
|
330
|
+
if (pool.running.size === 0 && pool.queued.length === 0)
|
|
331
|
+
this.pools.delete(pool.key);
|
|
332
|
+
}
|
|
333
|
+
poolClosedError(reason) {
|
|
334
|
+
return reason === 'auth_gate'
|
|
335
|
+
? new AdapterSchedulerError('ADAPTER_POOL_AUTH_GATE', 'Adapter pool stopped at an authentication or verification gate')
|
|
336
|
+
: new AdapterSchedulerError('ADAPTER_POOL_RATE_LIMITED', 'Adapter pool stopped after account rate limiting');
|
|
337
|
+
}
|
|
338
|
+
validateRequest(request) {
|
|
339
|
+
if (!request.requestId || !request.contextId || !request.site || !request.adapterSession || !request.sessionKey) {
|
|
340
|
+
throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter lease request');
|
|
341
|
+
}
|
|
342
|
+
if (!Number.isInteger(request.queueTimeoutMs) || request.queueTimeoutMs < 1) {
|
|
343
|
+
throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter queue timeout');
|
|
344
|
+
}
|
|
345
|
+
if (!Number.isInteger(request.maxParallel) || request.maxParallel < 1 || request.maxParallel > this.runtimeCeiling) {
|
|
346
|
+
throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter concurrency limit');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides a typed send() function that posts a Command and returns a Result.
|
|
5
5
|
*/
|
|
6
|
+
import type { AdapterLease, AdapterLeaseRelease, AdapterLeaseRequest, AdapterResourceGrant } from '../adapter-scheduler.js';
|
|
6
7
|
export interface DaemonCommand {
|
|
7
8
|
id: string;
|
|
8
9
|
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'ima-auth-start' | 'ima-auth-read' | 'ima-reader-request' | 'ima-auth-release' | 'wait-download' | 'cdp' | 'frames';
|
|
@@ -47,6 +48,8 @@ export interface DaemonCommand {
|
|
|
47
48
|
frameIndex?: number;
|
|
48
49
|
/** Browser profile/context to route the command to. */
|
|
49
50
|
contextId?: string;
|
|
51
|
+
/** Active Adapter lease used by the daemon to fence every browser action. */
|
|
52
|
+
adapterLease?: AdapterLease;
|
|
50
53
|
}
|
|
51
54
|
export interface DaemonResult {
|
|
52
55
|
id: string;
|
|
@@ -107,6 +110,12 @@ export type DaemonStatusProbe = {
|
|
|
107
110
|
} | {
|
|
108
111
|
kind: 'network_error';
|
|
109
112
|
};
|
|
113
|
+
export declare function acquireAdapterLease(request: AdapterLeaseRequest): Promise<AdapterLease>;
|
|
114
|
+
export declare function heartbeatAdapterLease(lease: AdapterLease): Promise<AdapterLease>;
|
|
115
|
+
export declare function releaseAdapterLease(release: AdapterLeaseRelease): Promise<boolean>;
|
|
116
|
+
export declare function cancelAdapterLease(requestId: string): Promise<boolean>;
|
|
117
|
+
export declare function acquireAdapterResources(lease: AdapterLease, keys: string[], timeoutMs: number): Promise<AdapterResourceGrant>;
|
|
118
|
+
export declare function releaseAdapterResources(lease: AdapterLease, grantId: string): Promise<boolean>;
|
|
110
119
|
export declare function probeDaemonStatus(opts?: {
|
|
111
120
|
timeout?: number;
|
|
112
121
|
contextId?: string;
|
|
@@ -139,6 +148,8 @@ export declare function getDaemonHealth(opts?: {
|
|
|
139
148
|
timeout?: number;
|
|
140
149
|
contextId?: string;
|
|
141
150
|
}): Promise<DaemonHealth>;
|
|
151
|
+
/** Resolve the concrete daemon profile used to key Adapter scheduler pools. */
|
|
152
|
+
export declare function resolveAdapterLeaseContextId(requestedContextId?: string): Promise<string>;
|
|
142
153
|
export declare function requestDaemonShutdown(opts?: {
|
|
143
154
|
timeout?: number;
|
|
144
155
|
}): Promise<boolean>;
|
|
@@ -7,6 +7,8 @@ import { sleep } from '../utils.js';
|
|
|
7
7
|
import { resolveDaemonPort } from './daemon-config.js';
|
|
8
8
|
import { classifyBrowserError } from './errors.js';
|
|
9
9
|
import { resolveProfileContextId } from './profile.js';
|
|
10
|
+
import { AdapterCoordinationError } from '../errors.js';
|
|
11
|
+
import { getAdapterExecutionContext } from '../adapter-execution-context.js';
|
|
10
12
|
const BYCLI_HEADERS = { 'X-byCLI': '1' };
|
|
11
13
|
let _idCounter = 0;
|
|
12
14
|
function generateId() {
|
|
@@ -44,6 +46,40 @@ async function consumeDaemonResponse(pathname, init, consume, port) {
|
|
|
44
46
|
async function requestDaemon(pathname, init) {
|
|
45
47
|
return consumeDaemonResponse(pathname, init, async (response) => response);
|
|
46
48
|
}
|
|
49
|
+
async function postAdapterLease(pathname, body, timeout) {
|
|
50
|
+
const response = await requestDaemon(pathname, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
timeout,
|
|
55
|
+
});
|
|
56
|
+
const envelope = await response.json();
|
|
57
|
+
if (!response.ok || envelope.ok !== true || envelope.data === undefined) {
|
|
58
|
+
throw new AdapterCoordinationError(envelope.errorCode ?? 'ADAPTER_QUEUE_RESET', envelope.error ?? 'Adapter scheduler request failed', true);
|
|
59
|
+
}
|
|
60
|
+
return envelope.data;
|
|
61
|
+
}
|
|
62
|
+
export function acquireAdapterLease(request) {
|
|
63
|
+
return postAdapterLease('/v1/adapter-leases/acquire', request, request.queueTimeoutMs + 5_000);
|
|
64
|
+
}
|
|
65
|
+
export function heartbeatAdapterLease(lease) {
|
|
66
|
+
return postAdapterLease('/v1/adapter-leases/heartbeat', lease, 5_000);
|
|
67
|
+
}
|
|
68
|
+
export async function releaseAdapterLease(release) {
|
|
69
|
+
const data = await postAdapterLease('/v1/adapter-leases/release', release, 5_000);
|
|
70
|
+
return data.released;
|
|
71
|
+
}
|
|
72
|
+
export async function cancelAdapterLease(requestId) {
|
|
73
|
+
const data = await postAdapterLease('/v1/adapter-leases/cancel', { requestId }, 5_000);
|
|
74
|
+
return data.cancelled;
|
|
75
|
+
}
|
|
76
|
+
export function acquireAdapterResources(lease, keys, timeoutMs) {
|
|
77
|
+
return postAdapterLease('/v1/adapter-resources/acquire', { lease, keys, timeoutMs }, timeoutMs + 5_000);
|
|
78
|
+
}
|
|
79
|
+
export async function releaseAdapterResources(lease, grantId) {
|
|
80
|
+
const data = await postAdapterLease('/v1/adapter-resources/release', { lease, grantId }, 5_000);
|
|
81
|
+
return data.released;
|
|
82
|
+
}
|
|
47
83
|
function errorCode(error) {
|
|
48
84
|
if (!error || typeof error !== 'object')
|
|
49
85
|
return undefined;
|
|
@@ -102,6 +138,16 @@ export async function getDaemonHealth(opts) {
|
|
|
102
138
|
return { state: 'no-extension', status };
|
|
103
139
|
return { state: 'ready', status };
|
|
104
140
|
}
|
|
141
|
+
/** Resolve the concrete daemon profile used to key Adapter scheduler pools. */
|
|
142
|
+
export async function resolveAdapterLeaseContextId(requestedContextId) {
|
|
143
|
+
const health = await getDaemonHealth({ contextId: requestedContextId });
|
|
144
|
+
if (health.state === 'ready' && health.status.contextId) {
|
|
145
|
+
return health.status.contextId;
|
|
146
|
+
}
|
|
147
|
+
throw new AdapterCoordinationError('ADAPTER_PROFILE_UNAVAILABLE', 'The browser daemon could not identify the authenticated profile for this Adapter session.', true, requestedContextId
|
|
148
|
+
? `Check that profile "${requestedContextId}" is connected.`
|
|
149
|
+
: 'Connect exactly one browser profile or pass --profile explicitly.');
|
|
150
|
+
}
|
|
105
151
|
export async function requestDaemonShutdown(opts) {
|
|
106
152
|
try {
|
|
107
153
|
const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
|
|
@@ -130,7 +176,13 @@ async function sendCommandRaw(action, params) {
|
|
|
130
176
|
: undefined;
|
|
131
177
|
const contextId = params.contextId ?? resolveProfileContextId();
|
|
132
178
|
const windowMode = params.windowMode ?? envWindowMode;
|
|
133
|
-
const
|
|
179
|
+
const adapterLease = getAdapterExecutionContext()?.lease;
|
|
180
|
+
const command = {
|
|
181
|
+
id, action, ...params,
|
|
182
|
+
...(contextId && { contextId }),
|
|
183
|
+
...(windowMode && { windowMode }),
|
|
184
|
+
...(adapterLease && { adapterLease }),
|
|
185
|
+
};
|
|
134
186
|
try {
|
|
135
187
|
const res = await requestDaemon('/command', {
|
|
136
188
|
method: 'POST',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const FOCUS_WINDOW_CAPABILITY = "focus-window-v1";
|
|
2
|
+
export declare const IMA_READER_CAPABILITY = "ima-reader-v1";
|
|
2
3
|
export declare const EXTENSION_CAPABILITY_MISSING_ERROR_CODE = "extension_capability_missing";
|
|
3
4
|
export declare const EXTENSION_CAPABILITY_MISSING_HTTP_STATUS = 412;
|
|
4
5
|
export declare function normalizeExtensionCapabilities(value: unknown): string[];
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
export const FOCUS_WINDOW_CAPABILITY = 'focus-window-v1';
|
|
2
|
+
export const IMA_READER_CAPABILITY = 'ima-reader-v1';
|
|
2
3
|
export const EXTENSION_CAPABILITY_MISSING_ERROR_CODE = 'extension_capability_missing';
|
|
3
4
|
export const EXTENSION_CAPABILITY_MISSING_HTTP_STATUS = 412;
|
|
5
|
+
const IMA_READER_ACTIONS = new Set([
|
|
6
|
+
'ima-auth-start',
|
|
7
|
+
'ima-auth-read',
|
|
8
|
+
'ima-reader-request',
|
|
9
|
+
'ima-auth-release',
|
|
10
|
+
]);
|
|
4
11
|
export function normalizeExtensionCapabilities(value) {
|
|
5
12
|
if (!Array.isArray(value))
|
|
6
13
|
return [];
|
|
7
14
|
return [...new Set(value.filter((entry) => typeof entry === 'string' && entry.length > 0))];
|
|
8
15
|
}
|
|
9
16
|
export function requiredExtensionCapability(command) {
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
if (command.action === 'tabs' && command.op === 'focus')
|
|
18
|
+
return FOCUS_WINDOW_CAPABILITY;
|
|
19
|
+
return typeof command.action === 'string' && IMA_READER_ACTIONS.has(command.action)
|
|
20
|
+
? IMA_READER_CAPABILITY
|
|
12
21
|
: undefined;
|
|
13
22
|
}
|
|
14
23
|
export function missingRequiredExtensionCapability(command, capabilities) {
|
|
@@ -16,7 +25,11 @@ export function missingRequiredExtensionCapability(command, capabilities) {
|
|
|
16
25
|
return required && !capabilities.includes(required) ? required : undefined;
|
|
17
26
|
}
|
|
18
27
|
export function extensionCapabilityHint(capability) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
if (capability === FOCUS_WINDOW_CAPABILITY) {
|
|
29
|
+
return 'Update and reload the byCLI Browser Bridge extension, then retry the login flow.';
|
|
30
|
+
}
|
|
31
|
+
if (capability === IMA_READER_CAPABILITY) {
|
|
32
|
+
return 'Update and reload the byCLI Browser Bridge extension with private ima reader support, then retry.';
|
|
33
|
+
}
|
|
34
|
+
return 'Update and reload the byCLI Browser Bridge extension, then retry.';
|
|
22
35
|
}
|
|
@@ -15,12 +15,13 @@ import { BasePage } from './base-page.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare class Page extends BasePage {
|
|
17
17
|
private readonly session;
|
|
18
|
-
|
|
18
|
+
contextId?: string | undefined;
|
|
19
19
|
private readonly windowMode?;
|
|
20
20
|
private readonly surface;
|
|
21
21
|
private readonly siteSession?;
|
|
22
22
|
private readonly _idleTimeout;
|
|
23
23
|
constructor(session: string, idleTimeout?: number, contextId?: string | undefined, windowMode?: "foreground" | "background" | undefined, surface?: 'browser' | 'adapter', siteSession?: "ephemeral" | "persistent" | undefined);
|
|
24
|
+
setContextId(contextId: string): void;
|
|
24
25
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
25
26
|
private _page;
|
|
26
27
|
private _networkCaptureUnsupported;
|
package/dist/src/browser/page.js
CHANGED
|
@@ -51,6 +51,9 @@ export class Page extends BasePage {
|
|
|
51
51
|
this.siteSession = siteSession;
|
|
52
52
|
this._idleTimeout = idleTimeout;
|
|
53
53
|
}
|
|
54
|
+
setContextId(contextId) {
|
|
55
|
+
this.contextId = contextId;
|
|
56
|
+
}
|
|
54
57
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
55
58
|
_page;
|
|
56
59
|
_networkCaptureUnsupported = false;
|
|
@@ -144,6 +144,10 @@ function knownCommandOptions(cmd) {
|
|
|
144
144
|
options.set('--site-session', 'required');
|
|
145
145
|
options.set('--keep-tab', 'required');
|
|
146
146
|
}
|
|
147
|
+
if (cmd.adapterConcurrency?.isolatedTabs === true) {
|
|
148
|
+
options.set('--adapter-session', 'required');
|
|
149
|
+
options.set('--adapter-queue-timeout', 'required');
|
|
150
|
+
}
|
|
147
151
|
for (const arg of cmd.args ?? []) {
|
|
148
152
|
if (arg.positional)
|
|
149
153
|
continue;
|
|
@@ -54,6 +54,11 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
54
54
|
.option('--site-session <mode>', 'Adapter site session lifecycle: ephemeral or persistent')
|
|
55
55
|
.option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
|
|
56
56
|
}
|
|
57
|
+
if (cmd.adapterConcurrency?.isolatedTabs === true) {
|
|
58
|
+
subCmd
|
|
59
|
+
.option('--adapter-session <name>', 'Named persistent Adapter tab session')
|
|
60
|
+
.option('--adapter-queue-timeout <seconds>', 'Seconds to wait for an Adapter command lease');
|
|
61
|
+
}
|
|
57
62
|
const originalHelpInformation = subCmd.helpInformation.bind(subCmd);
|
|
58
63
|
subCmd.helpInformation = ((contextOptions) => {
|
|
59
64
|
const format = getRequestedHelpFormat();
|
|
@@ -111,6 +116,12 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
111
116
|
...(hasBrowserCapability(cmd) && typeof optionsRecord.window === 'string' ? { windowMode: optionsRecord.window } : {}),
|
|
112
117
|
...(hasBrowserCapability(cmd) && typeof optionsRecord.siteSession === 'string' ? { siteSession: optionsRecord.siteSession } : {}),
|
|
113
118
|
...(hasBrowserCapability(cmd) && typeof optionsRecord.keepTab === 'string' ? { keepTab: optionsRecord.keepTab } : {}),
|
|
119
|
+
...(cmd.adapterConcurrency?.isolatedTabs === true && typeof optionsRecord.adapterSession === 'string'
|
|
120
|
+
? { adapterSession: optionsRecord.adapterSession }
|
|
121
|
+
: {}),
|
|
122
|
+
...(cmd.adapterConcurrency?.isolatedTabs === true && typeof optionsRecord.adapterQueueTimeout === 'string'
|
|
123
|
+
? { adapterQueueTimeout: optionsRecord.adapterQueueTimeout }
|
|
124
|
+
: {}),
|
|
114
125
|
});
|
|
115
126
|
if (result === null || result === undefined) {
|
|
116
127
|
return;
|