@sovovs/bycli 2.1.42 → 2.1.44

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.
@@ -129,7 +129,7 @@ function transportError(error, credentials) {
129
129
  if (isWechatVerificationResponse(redactedMessage, redactedHint)) {
130
130
  return new AuthRequiredError(
131
131
  DOMAIN,
132
- 'WeChat article index requires environment verification. Complete it in the open browser tab and run the command again.',
132
+ 'WeChat article index requires environment verification. Complete it in the retained browser tab and explicitly confirm completion to the caller.',
133
133
  );
134
134
  }
135
135
  return new CommandExecutionError(
@@ -71,7 +71,7 @@ export async function resolveWechatArticleUrl(page, rawUrl) {
71
71
  if (/验证码|安全验证|异常访问|访问过于频繁|请输入验证码/.test(text)) {
72
72
  throw new AuthRequiredError(
73
73
  'weixin.sogou.com',
74
- 'Sogou Weixin requires verification. Complete it in the open browser tab and run the command again.',
74
+ 'Sogou Weixin requires verification. Complete it in the retained browser tab and explicitly confirm completion to the caller.',
75
75
  );
76
76
  }
77
77
  if (!isTrustedWechatArticleUrl(result?.finalUrl)) {
@@ -131,7 +131,7 @@ export async function resolveBrowserCredentials(page, options = {}) {
131
131
 
132
132
  throw new AuthRequiredError(
133
133
  DOMAIN,
134
- 'WeChat login is required. The login tab is open; complete QR-code login and run the command again.',
134
+ 'WeChat login is required. Complete QR-code login in the retained browser tab and explicitly confirm completion to the caller.',
135
135
  );
136
136
  }
137
137
  }
@@ -144,7 +144,7 @@ export async function searchSogouArticlePage(page, { query, pageNo, preloadedUrl
144
144
  if (payload.blocked) {
145
145
  throw new AuthRequiredError(
146
146
  SOGOU_WEIXIN_DOMAIN,
147
- 'Sogou Weixin requires verification. Complete it in the open browser tab and run the command again.',
147
+ 'Sogou Weixin requires verification. Complete it in the retained browser tab and explicitly confirm completion to the caller.',
148
148
  );
149
149
  }
150
150
  if (payload.invalidCount > 0) {
@@ -233,7 +233,7 @@ cli({
233
233
  if (data?.errorHint === 'environment verification required') {
234
234
  throw new AuthRequiredError(
235
235
  'mp.weixin.qq.com',
236
- 'WeChat article page requires environment verification. Complete it in the open browser tab and run the command again.',
236
+ 'WeChat article page requires environment verification. Complete it in the retained browser tab and explicitly confirm completion to the caller.',
237
237
  );
238
238
  }
239
239
  const rows = await downloadArticle({
@@ -120,7 +120,7 @@ export async function fetchArticleHtmlInBrowser(article, page) {
120
120
  if (result?.accessIssue) {
121
121
  throw new AuthRequiredError(
122
122
  DOMAIN,
123
- 'WeChat article page requires environment verification. Complete it in the open browser tab and run the command again.',
123
+ 'WeChat article page requires environment verification. Complete it in the retained browser tab and explicitly confirm completion to the caller.',
124
124
  );
125
125
  }
126
126
  if (!isTrustedWechatArticleUrl(result?.finalUrl)) {
@@ -40,12 +40,14 @@ export interface AdapterSchedulerOptions {
40
40
  leaseExpiryMs?: number;
41
41
  runtimeCeiling?: number;
42
42
  releasedLeaseRetentionMs?: number;
43
+ startIntervalMs?: number;
43
44
  }
44
45
  export declare class AdapterScheduler {
45
46
  private readonly now;
46
47
  private readonly leaseExpiryMs;
47
48
  private readonly runtimeCeiling;
48
49
  private readonly releasedLeaseRetentionMs;
50
+ private readonly startIntervalMs;
49
51
  private readonly pools;
50
52
  private readonly releasedLeaseIds;
51
53
  private readonly generations;
@@ -74,6 +76,9 @@ export declare class AdapterScheduler {
74
76
  grants: number;
75
77
  };
76
78
  private schedule;
79
+ private rejectExpiredQueued;
80
+ private clearPoolTimer;
81
+ private armPoolTimer;
77
82
  private scheduleResources;
78
83
  private releaseAllResourcesForLease;
79
84
  private pruneReleasedLeaseIds;
@@ -12,6 +12,7 @@ export class AdapterScheduler {
12
12
  leaseExpiryMs;
13
13
  runtimeCeiling;
14
14
  releasedLeaseRetentionMs;
15
+ startIntervalMs;
15
16
  pools = new Map();
16
17
  releasedLeaseIds = new Map();
17
18
  generations = new Map();
@@ -20,10 +21,15 @@ export class AdapterScheduler {
20
21
  resourceQueue = [];
21
22
  sequence = 0;
22
23
  constructor(options = {}) {
24
+ const startIntervalMs = options.startIntervalMs ?? 5_000;
25
+ if (!Number.isInteger(startIntervalMs) || startIntervalMs < 0) {
26
+ throw new Error('Adapter start interval must be a non-negative integer');
27
+ }
23
28
  this.now = options.now ?? Date.now;
24
29
  this.leaseExpiryMs = options.leaseExpiryMs ?? 45_000;
25
30
  this.runtimeCeiling = options.runtimeCeiling ?? 3;
26
31
  this.releasedLeaseRetentionMs = options.releasedLeaseRetentionMs ?? 300_000;
32
+ this.startIntervalMs = startIntervalMs;
27
33
  }
28
34
  acquire(request) {
29
35
  this.validateRequest(request);
@@ -73,6 +79,7 @@ export class AdapterScheduler {
73
79
  this.releasedLeaseIds.set(lease.leaseId, this.now() + this.releasedLeaseRetentionMs);
74
80
  if (release.reason === 'auth_gate' || release.reason === 'rate_limited') {
75
81
  pool.closed = release.reason;
82
+ this.clearPoolTimer(pool);
76
83
  const error = this.poolClosedError(release.reason);
77
84
  for (const pending of pool.queued.splice(0)) {
78
85
  if (pending.timer)
@@ -144,14 +151,7 @@ export class AdapterScheduler {
144
151
  const now = this.now();
145
152
  this.pruneReleasedLeaseIds(now);
146
153
  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
- }
154
+ this.rejectExpiredQueued(pool, now);
155
155
  for (const lease of [...pool.running.values()]) {
156
156
  if (lease.heartbeatDeadline > now)
157
157
  continue;
@@ -176,6 +176,7 @@ export class AdapterScheduler {
176
176
  }
177
177
  reset() {
178
178
  for (const pool of this.pools.values()) {
179
+ this.clearPoolTimer(pool);
179
180
  for (const pending of pool.queued) {
180
181
  if (pending.timer)
181
182
  clearTimeout(pending.timer);
@@ -209,34 +210,69 @@ export class AdapterScheduler {
209
210
  };
210
211
  }
211
212
  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 });
213
+ if (pool.closed)
214
+ return;
215
+ const now = this.now();
216
+ this.rejectExpiredQueued(pool, now);
217
+ if (pool.running.size >= pool.maxParallel)
218
+ return;
219
+ const eligible = pool.queued
220
+ .filter(entry => !pool.activeSessions.has(entry.request.adapterSession))
221
+ .sort((a, b) => a.enqueuedAt - b.enqueuedAt || a.sequence - b.sequence)[0];
222
+ if (!eligible)
223
+ return;
224
+ if (now < pool.nextGrantAt) {
225
+ this.armPoolTimer(pool, pool.nextGrantAt);
226
+ return;
239
227
  }
228
+ pool.queued.splice(pool.queued.indexOf(eligible), 1);
229
+ if (eligible.timer)
230
+ clearTimeout(eligible.timer);
231
+ const grantedAt = now;
232
+ const lease = {
233
+ leaseId: crypto.randomUUID(),
234
+ requestId: eligible.request.requestId,
235
+ poolKey: pool.key,
236
+ contextId: eligible.request.contextId,
237
+ surface: 'adapter',
238
+ site: eligible.request.site,
239
+ adapterSession: eligible.request.adapterSession,
240
+ sessionKey: eligible.request.sessionKey,
241
+ generation: pool.generation,
242
+ grantedAt,
243
+ heartbeatDeadline: grantedAt + this.leaseExpiryMs,
244
+ };
245
+ pool.nextGrantAt = grantedAt + this.startIntervalMs;
246
+ pool.running.set(lease.leaseId, lease);
247
+ pool.activeSessions.add(lease.adapterSession);
248
+ eligible.resolve({ ...lease });
249
+ this.schedule(pool);
250
+ }
251
+ rejectExpiredQueued(pool, now) {
252
+ for (const pending of [...pool.queued]) {
253
+ if (pending.deadline > now)
254
+ continue;
255
+ pool.queued.splice(pool.queued.indexOf(pending), 1);
256
+ if (pending.timer)
257
+ clearTimeout(pending.timer);
258
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_TIMEOUT', 'Timed out waiting for an Adapter command lease'));
259
+ }
260
+ }
261
+ clearPoolTimer(pool) {
262
+ if (pool.scheduleTimer)
263
+ clearTimeout(pool.scheduleTimer);
264
+ pool.scheduleTimer = undefined;
265
+ }
266
+ armPoolTimer(pool, dueAt) {
267
+ if (pool.scheduleTimer)
268
+ return;
269
+ pool.scheduleTimer = setTimeout(() => {
270
+ pool.scheduleTimer = undefined;
271
+ if (this.pools.get(pool.key) !== pool)
272
+ return;
273
+ this.sweepExpired();
274
+ }, Math.max(0, dueAt - this.now()));
275
+ pool.scheduleTimer.unref?.();
240
276
  }
241
277
  scheduleResources() {
242
278
  for (const pending of [...this.resourceQueue].sort((a, b) => a.sequence - b.sequence)) {
@@ -319,6 +355,7 @@ export class AdapterScheduler {
319
355
  key,
320
356
  generation,
321
357
  maxParallel: Math.min(request.maxParallel, this.runtimeCeiling),
358
+ nextGrantAt: 0,
322
359
  running: new Map(),
323
360
  activeSessions: new Set(),
324
361
  queued: [],
@@ -327,8 +364,14 @@ export class AdapterScheduler {
327
364
  return pool;
328
365
  }
329
366
  removeDrainedPool(pool) {
330
- if (pool.running.size === 0 && pool.queued.length === 0)
331
- this.pools.delete(pool.key);
367
+ if (pool.running.size !== 0 || pool.queued.length !== 0)
368
+ return;
369
+ if (!pool.closed && this.now() < pool.nextGrantAt) {
370
+ this.armPoolTimer(pool, pool.nextGrantAt);
371
+ return;
372
+ }
373
+ this.clearPoolTimer(pool);
374
+ this.pools.delete(pool.key);
332
375
  }
333
376
  poolClosedError(reason) {
334
377
  return reason === 'auth_gate'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.42",
3
+ "version": "2.1.44",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },