@weapp-vite/miniprogram-automator 1.2.23 → 1.4.0

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/dist/index.mjs CHANGED
@@ -1,31 +1,49 @@
1
+ import { OPERATION_TIMEOUT_CODE, OperationLifecycle, classifyOperationError, isRecoverableOperationError, readWechatLoginState } from "./operation.mjs";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import net from "node:net";
3
4
  import process from "node:process";
4
- import { spawn } from "node:child_process";
5
5
  import path from "node:path";
6
6
  import { EventEmitter } from "node:events";
7
7
  import { createDebug } from "obug";
8
8
  import WebSocket from "ws";
9
+ import { stripVTControlCharacters } from "node:util";
9
10
  import fs from "node:fs/promises";
10
11
  import os from "node:os";
12
+ import { spawn } from "node:child_process";
11
13
  import { decodeQrCodeFromBase64, renderTerminalQrCode } from "@weapp-vite/qr";
12
14
  import { Buffer } from "node:buffer";
13
15
  //#region src/internal/compat/async.ts
14
16
  /**
15
17
  * @file 异步控制工具。
16
18
  */
17
- /** sleep 的方法封装。 */
18
- function sleep$1(timeout) {
19
- return new Promise((resolve) => setTimeout(resolve, timeout));
19
+ /** 可取消的延迟,取消时释放自身计时器。 */
20
+ function sleep$1(timeout, signal) {
21
+ signal?.throwIfAborted();
22
+ return new Promise((resolve, reject) => {
23
+ let timer;
24
+ function abort() {
25
+ clearTimeout(timer);
26
+ signal?.removeEventListener("abort", abort);
27
+ reject(signal?.reason);
28
+ }
29
+ timer = setTimeout(() => {
30
+ signal?.removeEventListener("abort", abort);
31
+ resolve();
32
+ }, timeout);
33
+ signal?.addEventListener("abort", abort, { once: true });
34
+ });
20
35
  }
21
- /** waitUntil 的方法封装。 */
22
- async function waitUntil(condition, timeout = 0, interval = 250) {
23
- const startTime = Date.now();
36
+ /** 条件轮询受同一个单调时钟 deadline 与取消信号约束。 */
37
+ async function waitUntil(condition, timeout = 0, interval = 250, signal) {
38
+ const deadlineAt = timeout ? performance.now() + timeout : Number.POSITIVE_INFINITY;
24
39
  while (true) {
40
+ signal?.throwIfAborted();
25
41
  const value = await condition();
42
+ signal?.throwIfAborted();
26
43
  if (value) return value;
27
- if (timeout && Date.now() - startTime >= timeout) throw new Error(`Wait timed out after ${timeout} ms`);
28
- await sleep$1(interval);
44
+ const remaining = deadlineAt - performance.now();
45
+ if (remaining <= 0) throw new Error(`Wait timed out after ${timeout} ms`);
46
+ await sleep$1(Math.min(interval, remaining), signal);
29
47
  }
30
48
  }
31
49
  //#endregion
@@ -218,6 +236,304 @@ function cmpVersion(versionA, versionB) {
218
236
  return 0;
219
237
  }
220
238
  //#endregion
239
+ //#region src/componentCaller/runtime.ts
240
+ /** 在同一个 Runtime 上下文持有真实 caller,不转用其他 ContextManager 的 token。 */
241
+ function createComponentCallerSession(expected, observerTimeout) {
242
+ const root = this;
243
+ const adapter = root.__glassEaselAdapter__;
244
+ const environment = adapter?.getEnv?.();
245
+ if (typeof environment?.getAdapterPage !== "function") throw new TypeError("Component caller backend selection is unavailable");
246
+ if (environment.getAdapterPage(expected.pageId)) return {
247
+ backend: "glass-easel",
248
+ dispose: () => ({
249
+ state: "disposed",
250
+ added: false,
251
+ removed: false
252
+ })
253
+ };
254
+ const bridge = root.WeixinJSBridge;
255
+ if (!bridge || typeof bridge.addSubscribeListener !== "function" || typeof bridge.removeSubscribeListener !== "function" || !adapter || typeof adapter.getVirtualDOM !== "function") throw new Error("Exact component caller bridge is unavailable");
256
+ const add = bridge.addSubscribeListener;
257
+ const remove = bridge.removeSubscribeListener;
258
+ const subscribe = bridge.subscribe;
259
+ const virtualDOM = adapter.getVirtualDOM();
260
+ const exparser = virtualDOM?.getExparser?.();
261
+ if (typeof virtualDOM?.getNodeById !== "function" || typeof virtualDOM.getNodeId !== "function" || typeof virtualDOM.getPageId !== "function" || typeof exparser?.Element?.getMethodCaller !== "function") throw new TypeError("Exact component caller mapping is unavailable");
262
+ let state = "created";
263
+ let caller;
264
+ let node;
265
+ let virtualId;
266
+ let added = false;
267
+ let removed = false;
268
+ let totalInvocations = 0;
269
+ let matchingInvocations = 0;
270
+ let timer;
271
+ const errors = [];
272
+ const eventName = "exparserComponentInvoker";
273
+ function identitiesMatch() {
274
+ return root.WeixinJSBridge === bridge && root.__glassEaselAdapter__ === adapter && bridge.subscribe === subscribe && bridge.addSubscribeListener === add && bridge.removeSubscribeListener === remove && adapter.getVirtualDOM() === virtualDOM && virtualDOM.getExparser() === exparser;
275
+ }
276
+ function snapshot() {
277
+ return {
278
+ state,
279
+ added,
280
+ removed,
281
+ totalInvocations,
282
+ matchingInvocations,
283
+ errors: [...errors],
284
+ identitiesMatch: identitiesMatch()
285
+ };
286
+ }
287
+ function listener(message, sender) {
288
+ totalInvocations += 1;
289
+ if (state !== "armed" && state !== "resolved") return;
290
+ if (message?.method !== "resolveComponentCaller") return;
291
+ matchingInvocations += 1;
292
+ try {
293
+ const args = message.params;
294
+ if (matchingInvocations !== 1 || !Array.isArray(args) || args.length !== 1 || args[0] === null || typeof args[0] !== "object" || Array.isArray(args[0])) throw new Error("Component caller resolver payload is ambiguous");
295
+ const params = args[0];
296
+ const id = params.nodeId;
297
+ if (!Number.isSafeInteger(message.callbackId) || message.callbackId <= 0 || sender !== expected.pageId || params.webviewId !== expected.pageId || !(typeof id === "string" && id.length > 0 || Number.isSafeInteger(id) && id >= 0)) throw new Error("Component caller resolver identity disagrees");
298
+ if (!identitiesMatch()) throw new Error("Component caller bridge identity changed");
299
+ const resolvedNode = virtualDOM.getNodeById(id, expected.pageId);
300
+ if (!resolvedNode || virtualDOM.getNodeId(resolvedNode, expected.pageId) !== id || virtualDOM.getPageId(resolvedNode) !== expected.pageId) throw new Error("Component caller virtual node identity disagrees");
301
+ const resolvedCaller = exparser.Element.getMethodCaller(resolvedNode);
302
+ if (!resolvedCaller || !["object", "function"].includes(typeof resolvedCaller)) throw new Error("Component caller is unavailable");
303
+ virtualId = id;
304
+ node = resolvedNode;
305
+ caller = resolvedCaller;
306
+ state = "resolved";
307
+ } catch (error) {
308
+ errors.push(error instanceof Error ? error.message : String(error));
309
+ state = "failed";
310
+ }
311
+ }
312
+ function removeListener() {
313
+ if (added && !removed) {
314
+ remove.call(bridge, eventName, listener);
315
+ removed = true;
316
+ }
317
+ if (timer !== void 0) {
318
+ clearTimeout(timer);
319
+ timer = void 0;
320
+ }
321
+ }
322
+ function dispose() {
323
+ try {
324
+ removeListener();
325
+ state = "disposed";
326
+ } finally {
327
+ caller = void 0;
328
+ node = void 0;
329
+ virtualId = void 0;
330
+ }
331
+ return {
332
+ state,
333
+ added,
334
+ removed,
335
+ totalInvocations,
336
+ matchingInvocations,
337
+ errors: [...errors]
338
+ };
339
+ }
340
+ return {
341
+ backend: "exparser",
342
+ arm() {
343
+ if (state !== "created") throw new Error("Component caller observer cannot be armed twice");
344
+ state = "armed";
345
+ timer = setTimeout(() => {
346
+ try {
347
+ dispose();
348
+ } catch (error) {
349
+ errors.push(error instanceof Error ? error.message : String(error));
350
+ state = "failed";
351
+ }
352
+ }, observerTimeout);
353
+ added = true;
354
+ add.call(bridge, eventName, listener);
355
+ return snapshot();
356
+ },
357
+ snapshot,
358
+ disarm() {
359
+ removeListener();
360
+ if (state !== "resolved" || matchingInvocations !== 1 || errors.length || !identitiesMatch()) throw new Error(`Exact component caller resolution failed: ${JSON.stringify(snapshot())}`);
361
+ state = "ready";
362
+ return snapshot();
363
+ },
364
+ invoke(method, args) {
365
+ if (state !== "ready" || !removed || errors.length || !identitiesMatch()) throw new Error("Component caller is not ready for a single invocation");
366
+ const pages = typeof getCurrentPages === "function" ? getCurrentPages() : [];
367
+ if (pages[pages.length - 1]?.__wxWebviewId__ !== expected.pageId || virtualDOM.getNodeById(virtualId, expected.pageId) !== node || virtualDOM.getNodeId(node, expected.pageId) !== virtualId || virtualDOM.getPageId(node) !== expected.pageId || exparser.Element.getMethodCaller(node) !== caller) throw new Error("Component caller changed before invocation");
368
+ state = "dispatched";
369
+ return caller[method](...args);
370
+ },
371
+ dispose
372
+ };
373
+ }
374
+ //#endregion
375
+ //#region src/componentCaller/index.ts
376
+ const queues = /* @__PURE__ */ new WeakMap();
377
+ const blocked = /* @__PURE__ */ new WeakMap();
378
+ const owners = /* @__PURE__ */ new WeakMap();
379
+ const TIMEOUT = 2500;
380
+ /** 关闭连接前确认全部自有 listener 已移除;失败时保留 token 与可重试的所有权。 */
381
+ function cleanupComponentCallers(connection) {
382
+ const pending = owners.get(connection);
383
+ if (!pending?.size) return;
384
+ return (async () => {
385
+ const errors = [];
386
+ for (const owner of [...pending]) try {
387
+ await owner.cleanup();
388
+ } catch (error) {
389
+ errors.push(error);
390
+ }
391
+ if (errors.length) throw new AggregateError(errors, "Component caller cleanup remains incomplete", { cause: errors[0] });
392
+ blocked.delete(connection);
393
+ })();
394
+ }
395
+ function result(response) {
396
+ if (response.exceptionDetails) throw new Error(response.exceptionDetails.text ?? "Component caller Runtime exception", { cause: response.exceptionDetails });
397
+ if (!response.result) throw new Error("Component caller Runtime result is missing");
398
+ return response.result;
399
+ }
400
+ function serializeArguments(args) {
401
+ const payload = JSON.parse(stringify({ params: { args } }));
402
+ return JSON.stringify(payload.params.args);
403
+ }
404
+ /** 保持原 SDK 的 returnByValue 解包,不等待业务返回的 Promise。 */
405
+ async function unwrap(connection, remote) {
406
+ if (remote.subtype === "null") return null;
407
+ if (remote.type === "undefined") return;
408
+ if (remote.value !== void 0) return remote.value;
409
+ if (remote.unserializableValue) {
410
+ const value = remote.unserializableValue;
411
+ if (/^-?\d+n$/.test(value)) return BigInt(value.slice(0, -1));
412
+ if ([
413
+ "NaN",
414
+ "Infinity",
415
+ "-Infinity",
416
+ "-0"
417
+ ].includes(value)) return Number(value);
418
+ throw new Error(`Unsupported component return value: ${value}`);
419
+ }
420
+ if (remote.objectId) try {
421
+ const converted = await connection.send("App.CDPCommand", {
422
+ domain: "Runtime",
423
+ method: "callFunctionOn",
424
+ params: {
425
+ objectId: remote.objectId,
426
+ functionDeclaration: "function () { try { return JSON.stringify(this) } catch { return String(this) } }",
427
+ returnByValue: true
428
+ }
429
+ }, { timeout: TIMEOUT });
430
+ if (converted.result?.value !== void 0) try {
431
+ return JSON.parse(converted.result.value);
432
+ } catch {
433
+ return converted.result.value;
434
+ }
435
+ } catch {}
436
+ return remote.description ?? `[${remote.type}]`;
437
+ }
438
+ async function invoke(connection, target, method, args, native) {
439
+ let token;
440
+ let owner;
441
+ const errors = [];
442
+ let cleanupFailed = false;
443
+ let nativeBackend = false;
444
+ let value;
445
+ const cdp = async (domain, command, params) => {
446
+ return await connection.send("App.CDPCommand", {
447
+ domain,
448
+ method: command,
449
+ params
450
+ }, { timeout: TIMEOUT });
451
+ };
452
+ const wrapper = async (body) => result(await cdp("Runtime", "callFunctionOn", {
453
+ objectId: token,
454
+ functionDeclaration: `function () { return ${body} }`,
455
+ arguments: [],
456
+ returnByValue: true
457
+ }));
458
+ try {
459
+ if (!Number.isSafeInteger(target.nodeId) || target.nodeId < 0 || !Number.isSafeInteger(target.pageId)) throw new Error("Invalid component caller native target");
460
+ if ((await connection.send("App.getCurrentPage", {}, { timeout: TIMEOUT })).pageId !== target.pageId) throw new Error("Component caller target is not on the current page");
461
+ const created = result(await cdp("Runtime", "evaluate", {
462
+ expression: `(${createComponentCallerSession.toString()})(${JSON.stringify(target)}, 10000)`,
463
+ returnByValue: false
464
+ }));
465
+ if (!created.objectId) throw new Error("Component caller session has no owned Runtime token");
466
+ token = created.objectId;
467
+ let cleaning;
468
+ let disposed = false;
469
+ let released = false;
470
+ owner = { cleanup() {
471
+ cleaning ??= (async () => {
472
+ if (released) return;
473
+ if (!disposed) {
474
+ const snapshot = (await wrapper("this.dispose()")).value;
475
+ if (snapshot?.state !== "disposed" || snapshot.added && !snapshot.removed) throw new Error("Component caller listener removal was not confirmed");
476
+ disposed = true;
477
+ }
478
+ await cdp("Runtime", "releaseObject", { objectId: token });
479
+ released = true;
480
+ owners.get(connection)?.delete(owner);
481
+ })().catch((error) => {
482
+ cleaning = void 0;
483
+ throw error;
484
+ });
485
+ return cleaning;
486
+ } };
487
+ let pending = owners.get(connection);
488
+ if (!pending) {
489
+ pending = /* @__PURE__ */ new Set();
490
+ owners.set(connection, pending);
491
+ }
492
+ pending.add(owner);
493
+ const backend = (await wrapper("this.backend")).value;
494
+ if (backend === "glass-easel") nativeBackend = true;
495
+ else if (backend === "exparser") {
496
+ const argumentSource = serializeArguments(args);
497
+ await wrapper("this.arm()");
498
+ await cdp("WxComponent", "resolveComponentCaller", { nodeId: target.nodeId });
499
+ await wrapper("this.disarm()");
500
+ value = await unwrap(connection, await wrapper(`this.invoke(${JSON.stringify(method)}, ${argumentSource})`));
501
+ } else throw new Error("Unknown component caller backend");
502
+ } catch (error) {
503
+ errors.push(error);
504
+ } finally {
505
+ if (owner) try {
506
+ await owner.cleanup();
507
+ } catch (error) {
508
+ errors.push(error);
509
+ cleanupFailed = true;
510
+ }
511
+ }
512
+ if (cleanupFailed) blocked.set(connection, new Error("Component caller cleanup failed; reconnect before invoking another component", { cause: errors[0] }));
513
+ if (errors.length === 1) throw errors[0];
514
+ if (errors.length) throw new AggregateError(errors, "Component caller invocation and cleanup failed", { cause: errors[0] });
515
+ if (nativeBackend) {
516
+ if (!native) throw new Error("Native component backend requires the original protocol");
517
+ return await native();
518
+ }
519
+ return value;
520
+ }
521
+ /** 同一连接串行观察 resolver,避免多个组件误认彼此的桥接消息。 */
522
+ async function invokeComponentMethod(connection, target, method, args, native) {
523
+ const pending = (queues.get(connection) ?? Promise.resolve()).then(async () => {
524
+ const failure = blocked.get(connection);
525
+ if (failure) throw failure;
526
+ return await invoke(connection, target, method, args, native);
527
+ });
528
+ const settled = pending.then(() => {}, () => {});
529
+ queues.set(connection, settled);
530
+ try {
531
+ return await pending;
532
+ } finally {
533
+ if (queues.get(connection) === settled) queues.delete(connection);
534
+ }
535
+ }
536
+ //#endregion
221
537
  //#region src/pageRouteSnapshot.ts
222
538
  const ROUTE_ELEMENT_SNAPSHOT_TIMEOUT = 5e3;
223
539
  const ROUTE_ELEMENT_SNAPSHOT_EMPTY_RETRIES = 2;
@@ -437,6 +753,14 @@ async function readRouteElementSnapshot(connection, route, query, selector, inde
437
753
  }
438
754
  //#endregion
439
755
  //#region src/Element.ts
756
+ function normalizeTouchOptions(options) {
757
+ const { changeTouches, ...payload } = options;
758
+ const changedTouches = options.changedTouches ?? changeTouches;
759
+ return changedTouches === void 0 ? payload : {
760
+ ...payload,
761
+ changedTouches
762
+ };
763
+ }
440
764
  const ELEMENT_QUERY_TIMEOUT = 2500;
441
765
  const ELEMENT_WXML_TIMEOUT = 8e3;
442
766
  /** Element 的实现。 */
@@ -542,16 +866,26 @@ var Element = class Element {
542
866
  await this.send("Element.triggerEvent", payload);
543
867
  }
544
868
  async touchstart(options = {}) {
545
- await this.send("Element.touchstart", options);
869
+ await this.send("Element.touchstart", normalizeTouchOptions(options));
546
870
  }
547
871
  async touchmove(options = {}) {
548
- await this.send("Element.touchmove", options);
872
+ await this.send("Element.touchmove", normalizeTouchOptions(options));
549
873
  }
550
874
  async touchend(options = {}) {
551
- await this.send("Element.touchend", options);
875
+ await this.send("Element.touchend", normalizeTouchOptions(options));
552
876
  }
553
877
  async dispatchEvent(options) {
554
- await this.send("Element.dispatchEvent", options);
878
+ const payload = normalizeTouchOptions(options);
879
+ if (!payload.$ && !payload.detail && (payload.eventName === "touchstart" || payload.eventName === "touchmove" || payload.eventName === "touchend") && (payload.touches !== void 0 || payload.changedTouches !== void 0)) {
880
+ const { eventName, ...touchOptions } = payload;
881
+ await this.send(`Element.${eventName}`, touchOptions);
882
+ return;
883
+ }
884
+ const { detail, ...event } = payload;
885
+ await this.send("Element.dispatchEvent", detail === void 0 ? event : {
886
+ ...event,
887
+ eventData: detail
888
+ });
555
889
  }
556
890
  async _property(name) {
557
891
  return await this.getter(name, "getProperties", "properties");
@@ -733,6 +1067,18 @@ var CustomElement = class extends Element {
733
1067
  return (await this.send("Element.getData", payload)).data;
734
1068
  }
735
1069
  async callMethod(method, ...args) {
1070
+ if (this.connection.prefersComponentCallerBridge) {
1071
+ const nodeId = this.nodeId === null ? NaN : Number(this.nodeId);
1072
+ return await invokeComponentMethod(this.connection, {
1073
+ nodeId,
1074
+ pageId: this.pageId
1075
+ }, method, args, async () => {
1076
+ return (await this.send("Element.callMethod", {
1077
+ method,
1078
+ args
1079
+ })).result;
1080
+ });
1081
+ }
736
1082
  return (await this.send("Element.callMethod", {
737
1083
  method,
738
1084
  args
@@ -811,6 +1157,9 @@ var ContextElement = class extends Element {
811
1157
  /** Transport 的实现。 */
812
1158
  var Transport = class extends EventEmitter {
813
1159
  ws;
1160
+ closed = false;
1161
+ terminating = false;
1162
+ closing;
814
1163
  constructor(ws) {
815
1164
  super();
816
1165
  this.ws = ws;
@@ -818,6 +1167,7 @@ var Transport = class extends EventEmitter {
818
1167
  this.emit("message", typeof data === "string" ? data : data.toString());
819
1168
  });
820
1169
  ws.on("close", () => {
1170
+ this.closed = true;
821
1171
  this.emit("close");
822
1172
  });
823
1173
  }
@@ -825,7 +1175,36 @@ var Transport = class extends EventEmitter {
825
1175
  this.ws.send(message);
826
1176
  }
827
1177
  close() {
828
- this.ws.close();
1178
+ if (this.closing) return this.closing;
1179
+ if (this.closed || this.ws.readyState === 3) return this.closing = Promise.resolve();
1180
+ this.closing = new Promise((resolve, reject) => {
1181
+ let timer;
1182
+ const finish = () => {
1183
+ clearTimeout(timer);
1184
+ this.off("close", finish);
1185
+ resolve();
1186
+ };
1187
+ timer = setTimeout(() => {
1188
+ this.off("close", finish);
1189
+ reject(Object.assign(/* @__PURE__ */ new Error("DevTools websocket did not close within 2000ms"), { code: "DEVTOOLS_DISCONNECT_TIMEOUT" }));
1190
+ }, 2e3);
1191
+ this.once("close", finish);
1192
+ try {
1193
+ if (!this.terminating) {
1194
+ this.terminating = true;
1195
+ this.ws.terminate();
1196
+ }
1197
+ } catch (error) {
1198
+ this.terminating = false;
1199
+ clearTimeout(timer);
1200
+ this.off("close", finish);
1201
+ reject(error);
1202
+ }
1203
+ }).catch((error) => {
1204
+ this.closing = void 0;
1205
+ throw error;
1206
+ });
1207
+ return this.closing;
829
1208
  }
830
1209
  };
831
1210
  //#endregion
@@ -840,13 +1219,22 @@ const CONNECT_TIMEOUT = 3e4;
840
1219
  /** Page frame 定向消息失效、需要切换到 App-service Page 协议的 DevTools 版本。 */
841
1220
  const APP_SERVICE_PAGE_PROTOCOL_VERSIONS = /* @__PURE__ */ new Set(["2.01.2510290"]);
842
1221
  /** 仅页面方法调用失效的版本,元素查询仍保留原生组件作用域。 */
843
- const APP_SERVICE_PAGE_METHOD_VERSIONS = /* @__PURE__ */ new Set(["2.02.2608070", "2.02.2609231"]);
1222
+ const APP_SERVICE_PAGE_METHOD_VERSIONS = /* @__PURE__ */ new Set([
1223
+ "2.02.2608070",
1224
+ "2.02.2608080",
1225
+ "2.02.2609231",
1226
+ "2.02.2610082",
1227
+ "2.02.2610092"
1228
+ ]);
844
1229
  /** Connection 的实现。 */
845
1230
  var Connection = class Connection extends EventEmitter {
846
1231
  transport;
1232
+ disposed = false;
1233
+ disposing;
847
1234
  callbacks = /* @__PURE__ */ new Map();
848
1235
  useAppServicePageProtocol = false;
849
1236
  useAppServicePageMethod = false;
1237
+ useComponentCallerBridge = false;
850
1238
  constructor(transport) {
851
1239
  super();
852
1240
  this.transport = transport;
@@ -854,6 +1242,7 @@ var Connection = class Connection extends EventEmitter {
854
1242
  transport.on("close", this.onClose);
855
1243
  }
856
1244
  send(method, params = {}, options = {}) {
1245
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(closeErrTip));
857
1246
  const id = uuid();
858
1247
  const payload = stringify({
859
1248
  id,
@@ -877,21 +1266,59 @@ var Connection = class Connection extends EventEmitter {
877
1266
  });
878
1267
  try {
879
1268
  this.transport.send(payload);
880
- } catch {
1269
+ } catch (cause) {
881
1270
  clearTimeout(timeout);
882
1271
  this.callbacks.delete(id);
883
- reject(/* @__PURE__ */ new Error(closeErrTip));
1272
+ reject(new Error(closeErrTip, { cause }));
884
1273
  }
885
1274
  });
886
1275
  }
887
1276
  dispose() {
888
- this.transport.close();
1277
+ if (this.disposing) return this.disposing;
1278
+ let resolve;
1279
+ let reject;
1280
+ this.disposing = new Promise((done, fail) => {
1281
+ resolve = done;
1282
+ reject = fail;
1283
+ }).catch((error) => {
1284
+ this.disposing = void 0;
1285
+ throw error;
1286
+ });
1287
+ const cleanup = cleanupComponentCallers(this);
1288
+ if (cleanup) cleanup.then(() => this.closeTransport(resolve, reject), reject);
1289
+ else this.closeTransport(resolve, reject);
1290
+ return this.disposing;
1291
+ }
1292
+ closeTransport(resolve, reject) {
1293
+ const errors = [];
1294
+ try {
1295
+ this.onClose();
1296
+ } catch (error) {
1297
+ errors.push(error);
1298
+ }
1299
+ this.transport.off("message", this.onMessage);
1300
+ this.transport.off("close", this.onClose);
1301
+ const finish = () => {
1302
+ if (errors.length === 1) reject(errors[0]);
1303
+ else if (errors.length) reject(new AggregateError(errors, "Connection close notification and transport cleanup failed", { cause: errors[0] }));
1304
+ else resolve();
1305
+ };
1306
+ try {
1307
+ Promise.resolve(this.transport.close()).then(finish, (error) => {
1308
+ errors.push(error);
1309
+ finish();
1310
+ });
1311
+ } catch (error) {
1312
+ errors.push(error);
1313
+ finish();
1314
+ }
889
1315
  }
890
1316
  /** 根据 DevTools 版本选择稳定的 Page 协议实现。 */
891
1317
  configureToolInfo(info) {
892
1318
  const version = String(info.version ?? "");
893
1319
  this.useAppServicePageProtocol = APP_SERVICE_PAGE_PROTOCOL_VERSIONS.has(version);
894
1320
  this.useAppServicePageMethod = this.useAppServicePageProtocol || APP_SERVICE_PAGE_METHOD_VERSIONS.has(version);
1321
+ this.useComponentCallerBridge = version === "2.02.2608080" && info.SDKVersion === "3.17.5";
895
1322
  }
896
1323
  get prefersAppServicePageProtocol() {
897
1324
  return this.useAppServicePageProtocol;
@@ -900,6 +1327,10 @@ var Connection = class Connection extends EventEmitter {
900
1327
  get prefersAppServicePageMethod() {
901
1328
  return this.useAppServicePageMethod;
902
1329
  }
1330
+ /** 仅为实测存在跨上下文 token 错绑的 IDE 与基础库启用真实 caller 桥接。 */
1331
+ get prefersComponentCallerBridge() {
1332
+ return this.useComponentCallerBridge;
1333
+ }
903
1334
  onMessage = (message) => {
904
1335
  debugProtocol(`${dateFormat("yyyy-mm-dd HH:MM:ss:l")} ◀ RECV ${message}`);
905
1336
  const { id, method, error, result, params } = JSON.parse(message);
@@ -912,47 +1343,87 @@ var Connection = class Connection extends EventEmitter {
912
1343
  this.callbacks.delete(id);
913
1344
  clearTimeout(callback.timeout);
914
1345
  if (error) {
915
- callback.reject(new Error(error.message || closeErrTip));
1346
+ const message = typeof error.message === "string" ? error.message : error.message === void 0 ? closeErrTip : JSON.stringify(error.message);
1347
+ callback.reject(new Error(message, { cause: error }));
916
1348
  return;
917
1349
  }
918
1350
  callback.resolve(result);
919
1351
  };
920
1352
  onClose = () => {
1353
+ if (this.disposed) return;
1354
+ this.disposed = true;
921
1355
  for (const callback of this.callbacks.values()) {
922
1356
  clearTimeout(callback.timeout);
923
1357
  callback.reject(/* @__PURE__ */ new Error(closeErrTip));
924
1358
  }
925
1359
  this.callbacks.clear();
1360
+ this.emit("close");
926
1361
  };
927
- static create(url, timeout = CONNECT_TIMEOUT) {
1362
+ static create(url, timeout = CONNECT_TIMEOUT, signal) {
1363
+ signal?.throwIfAborted();
928
1364
  return new Promise((resolve, reject) => {
929
1365
  const ws = new WebSocket(url);
930
1366
  let settled = false;
931
- const timer = setTimeout(() => {
1367
+ let timer;
1368
+ let onAbort;
1369
+ let onOpen;
1370
+ let onClose;
1371
+ function cleanup() {
1372
+ clearTimeout(timer);
1373
+ signal?.removeEventListener("abort", onAbort);
1374
+ ws.off("open", onOpen);
1375
+ ws.off("close", onClose);
1376
+ }
1377
+ function fail(error) {
932
1378
  if (settled) return;
933
1379
  settled = true;
934
- ws.close();
935
- reject(/* @__PURE__ */ new Error(`Timed out connecting to DevTools websocket ${url} after ${timeout}ms`));
936
- }, timeout);
937
- ws.on("open", () => {
1380
+ cleanup();
1381
+ ws.terminate();
1382
+ reject(error);
1383
+ }
1384
+ onAbort = () => {
1385
+ fail(signal?.reason);
1386
+ };
1387
+ onClose = () => {
1388
+ fail(/* @__PURE__ */ new Error("DevTools websocket closed before handshake completed"));
1389
+ };
1390
+ onOpen = () => {
938
1391
  if (settled) return;
939
1392
  settled = true;
940
- clearTimeout(timer);
1393
+ cleanup();
941
1394
  resolve(new Connection(new Transport(ws)));
942
- });
943
- ws.on("error", (error) => {
944
- if (settled) return;
945
- settled = true;
946
- clearTimeout(timer);
947
- reject(error);
948
- });
1395
+ };
1396
+ timer = setTimeout(() => fail(/* @__PURE__ */ new Error(`Timed out connecting to DevTools websocket ${url} after ${timeout}ms`)), timeout);
1397
+ ws.on("open", onOpen);
1398
+ ws.on("error", fail);
1399
+ ws.on("close", onClose);
1400
+ signal?.addEventListener("abort", onAbort, { once: true });
1401
+ if (signal?.aborted) onAbort();
949
1402
  });
950
1403
  }
951
1404
  };
952
1405
  //#endregion
953
1406
  //#region src/headless.ts
954
1407
  async function launchHeadlessAutomator(options) {
955
- return await (await import("./launch-CSjvON-f.mjs")).launch({ projectPath: options.projectPath });
1408
+ return await (await import("./launch-ElaGbFxh.mjs")).launch({ projectPath: options.projectPath });
1409
+ }
1410
+ //#endregion
1411
+ //#region src/launcher/diagnostics.ts
1412
+ /** 将本次 CLI 的有界输出附到原始错误,避免泄露登录票据及账号参数。 */
1413
+ function attachCliDiagnostics(error, state, secrets = []) {
1414
+ const original = error instanceof Error ? error : new Error("Failed to launch WeChat DevTools", { cause: error });
1415
+ if (original.cliOutput !== void 0) return original;
1416
+ let output = stripVTControlCharacters(state.output).trim();
1417
+ for (const secret of secrets.filter(Boolean).sort((a, b) => b.length - a.length)) output = output.split(secret).join("[redacted]");
1418
+ output = output.replace(/(authorization["']?\s*[:=]\s*["']?)(?:Bearer|Basic)\s+[^\s"',;}]+/gi, "$1[redacted]");
1419
+ output = output.replace(/((?:--)?(?:auto-account|account|ticket|access[_-]?token|refresh[_-]?token|token|authorization)["']?\s*(?:[:=]\s*|\s))(["']?)([^\s"',;}]+)\2/gi, "$1[redacted]");
1420
+ if (!output) return original;
1421
+ const truncated = state.outputTruncated || output.length > 32768;
1422
+ output = output.slice(-32768);
1423
+ const target = Object.isExtensible(original) ? original : new Error(original.message, { cause: original });
1424
+ target.cliOutput = output;
1425
+ target.message += `\ncli output${truncated ? " (truncated; last 32768 characters)" : ""}:\n${output}`;
1426
+ return target;
956
1427
  }
957
1428
  //#endregion
958
1429
  //#region src/launcher/portLease.ts
@@ -978,7 +1449,7 @@ function isProcessAlive(pid) {
978
1449
  }
979
1450
  }
980
1451
  async function removePortLock(leasePath) {
981
- await fs.rm(leasePath, { force: true }).catch(() => {});
1452
+ await fs.rm(leasePath, { force: true });
982
1453
  }
983
1454
  async function removeStalePortLock(leasePath) {
984
1455
  try {
@@ -1010,14 +1481,18 @@ async function createPortLock(port) {
1010
1481
  return leasePath;
1011
1482
  }
1012
1483
  function createLease(port, leasePath) {
1013
- let released = false;
1484
+ let releasing;
1014
1485
  return {
1015
1486
  path: leasePath,
1016
1487
  port,
1017
- release: async () => {
1018
- if (released) return;
1019
- released = true;
1020
- if (leasePath) await removePortLock(leasePath);
1488
+ release: () => {
1489
+ releasing ??= (async () => {
1490
+ if (leasePath) await removePortLock(leasePath);
1491
+ })().catch((error) => {
1492
+ releasing = void 0;
1493
+ throw error;
1494
+ });
1495
+ return releasing;
1021
1496
  }
1022
1497
  };
1023
1498
  }
@@ -1064,7 +1539,10 @@ async function enableAutomatorViaHttp(options) {
1064
1539
  if (options.account) endpoint.searchParams.set("account", options.account);
1065
1540
  if (options.ticket) endpoint.searchParams.set("ticket", options.ticket);
1066
1541
  if (options.trustProject) endpoint.searchParams.set("trustProject", "true");
1067
- const response = await fetch(endpoint, { redirect: "follow" });
1542
+ const response = await fetch(endpoint, {
1543
+ redirect: "follow",
1544
+ signal: options.signal
1545
+ });
1068
1546
  const body = await response.text();
1069
1547
  if (!response.ok) throw new Error(`WeChat DevTools HTTP automator fallback failed with status ${response.status}`);
1070
1548
  let result;
@@ -1073,6 +1551,7 @@ async function enableAutomatorViaHttp(options) {
1073
1551
  } catch {
1074
1552
  throw new Error("WeChat DevTools HTTP automator fallback returned invalid JSON");
1075
1553
  }
1554
+ if (result && typeof result === "object" && "code" in result && Number(result.code) === 10) throw Object.assign(/* @__PURE__ */ new Error("DEVTOOLS_LOGIN_REQUIRED: need re-login"), { code: 10 });
1076
1555
  if (result && typeof result === "object" && "autoPort" in result) {
1077
1556
  const autoPort = Number(result.autoPort);
1078
1557
  if (!Number.isInteger(autoPort) || autoPort <= 0 || autoPort > 65535) throw new Error("WeChat DevTools HTTP automator fallback returned invalid autoPort");
@@ -1106,8 +1585,231 @@ function resolveWechatDevtoolsBootstrapArgs(args) {
1106
1585
  return bootstrapArgs;
1107
1586
  }
1108
1587
  //#endregion
1588
+ //#region src/launcher/process.ts
1589
+ const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
1590
+ function shouldUseWindowsCommandShell(cliPath) {
1591
+ return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
1592
+ }
1593
+ function escapeWindowsCmdArg(arg) {
1594
+ const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
1595
+ return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
1596
+ }
1597
+ function resolveWindowsBatchSpawn(cliPath, args) {
1598
+ return {
1599
+ file: process.env.ComSpec || "cmd.exe",
1600
+ args: [
1601
+ "/d",
1602
+ "/s",
1603
+ "/c",
1604
+ `"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
1605
+ ]
1606
+ };
1607
+ }
1608
+ /** 只持有本次 CLI 子进程句柄;退出后不通过旧 PID 查找或终止宿主。 */
1609
+ function spawnWechatCli(cliPath, args, cwd, scope) {
1610
+ scope.throwIfAborted();
1611
+ const state = {
1612
+ error: null,
1613
+ success: false,
1614
+ exited: false,
1615
+ output: "",
1616
+ outputTruncated: false
1617
+ };
1618
+ const bootstrapArgs = resolveWechatDevtoolsBootstrapArgs(args);
1619
+ const target = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, bootstrapArgs) : {
1620
+ file: cliPath,
1621
+ args: bootstrapArgs
1622
+ };
1623
+ const child = spawn(target.file, target.args, {
1624
+ stdio: [
1625
+ "ignore",
1626
+ "pipe",
1627
+ "pipe"
1628
+ ],
1629
+ cwd: cwd || void 0,
1630
+ ...shouldUseWindowsCommandShell(cliPath) ? {
1631
+ windowsHide: true,
1632
+ windowsVerbatimArguments: true
1633
+ } : {}
1634
+ });
1635
+ let released;
1636
+ const release = () => {
1637
+ if (!released) released = new Promise((resolve) => {
1638
+ if (state.exited || !child.pid || child.exitCode !== null || child.signalCode !== null) {
1639
+ resolve();
1640
+ return;
1641
+ }
1642
+ child.once("close", () => resolve());
1643
+ child.kill("SIGKILL");
1644
+ });
1645
+ return released;
1646
+ };
1647
+ const disown = scope.own(release, "cli-process");
1648
+ child.on("error", (error) => {
1649
+ state.error = error;
1650
+ });
1651
+ const output = (chunk) => {
1652
+ const combined = state.output + String(chunk);
1653
+ state.outputTruncated ||= combined.length > 32768;
1654
+ state.output = combined.slice(-32768);
1655
+ };
1656
+ child.stdout?.on("data", output);
1657
+ child.stderr?.on("data", output);
1658
+ child.on("exit", (code, signal) => {
1659
+ disown();
1660
+ state.exited = true;
1661
+ if (code !== 0 || signal) state.error = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
1662
+ else state.success = true;
1663
+ });
1664
+ child.unref();
1665
+ return Object.assign(state, { release });
1666
+ }
1667
+ //#endregion
1109
1668
  //#region package.json
1110
- var version = "1.2.23";
1669
+ var version = "1.4.0";
1670
+ //#endregion
1671
+ //#region src/appServiceHeap.ts
1672
+ function isRecord$1(value) {
1673
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1674
+ }
1675
+ function unsupported$1(reason) {
1676
+ return {
1677
+ status: "unsupported",
1678
+ source: "appservice-cdp-runtime",
1679
+ reason
1680
+ };
1681
+ }
1682
+ /** 只读取 AppService 当前 V8 堆,不调用 GC,也不把协议故障或空响应当成零内存。 */
1683
+ async function readAppServiceHeapUsage(send, options = {}) {
1684
+ const timeout = options.timeout ?? 2500;
1685
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new TypeError("AppService heap timeout must be finite and positive");
1686
+ let response;
1687
+ try {
1688
+ response = await send("App.CDPCommand", {
1689
+ domain: "Runtime",
1690
+ method: "getHeapUsage",
1691
+ params: {}
1692
+ }, { timeout });
1693
+ } catch (error) {
1694
+ if (error instanceof Error && (error.message === "appservice App.CDPCommand unimplemented" || error.message === "Method not implemented.")) return unsupported$1("protocol-unimplemented");
1695
+ if (error instanceof Error && error.message === "'Runtime.getHeapUsage' wasn't found") return unsupported$1("method-not-found");
1696
+ throw error;
1697
+ }
1698
+ if (isRecord$1(response) && isRecord$1(response.error) && response.error.code === -32601) return unsupported$1("method-not-found");
1699
+ if (!isRecord$1(response) || response.error || response.exceptionDetails || typeof response.usedSize !== "number" || !Number.isFinite(response.usedSize) || response.usedSize < 0 || typeof response.totalSize !== "number" || !Number.isFinite(response.totalSize) || response.totalSize < response.usedSize) throw new Error("Invalid AppService Runtime.getHeapUsage response: expected finite non-negative usedSize <= totalSize");
1700
+ return {
1701
+ status: "available",
1702
+ source: "appservice-cdp-runtime",
1703
+ usedSize: response.usedSize,
1704
+ totalSize: response.totalSize
1705
+ };
1706
+ }
1707
+ //#endregion
1708
+ //#region src/cdp/index.ts
1709
+ /** 单个连接共享 CDP 启用状态与订阅;不创建额外 WebSocket。 */
1710
+ var CDPController = class {
1711
+ connection;
1712
+ send;
1713
+ enabling;
1714
+ enabled = false;
1715
+ stopped = false;
1716
+ subscriptions = /* @__PURE__ */ new Set();
1717
+ session;
1718
+ constructor(connection, send) {
1719
+ this.connection = connection;
1720
+ this.send = send;
1721
+ connection.on("close", this.stop);
1722
+ this.session = {
1723
+ enable: (options) => this.enable(options),
1724
+ sendCommand: (domain, method, params = {}, options) => this.request("App.CDPCommand", {
1725
+ domain,
1726
+ method,
1727
+ params
1728
+ }, options),
1729
+ listProtocol: (options) => this.request("App.CDPListProtocol", {}, options),
1730
+ onEvent: (listener) => this.onEvent(listener)
1731
+ };
1732
+ }
1733
+ enable(options) {
1734
+ if (this.stopped) return Promise.reject(/* @__PURE__ */ new Error("CDP connection closed"));
1735
+ try {
1736
+ this.validateTimeout(options);
1737
+ } catch (error) {
1738
+ return Promise.reject(error);
1739
+ }
1740
+ if (!this.enabling) {
1741
+ let pending;
1742
+ try {
1743
+ pending = this.send("App.CDPEnable", {}, options);
1744
+ } catch (error) {
1745
+ pending = Promise.reject(error);
1746
+ }
1747
+ this.enabling = pending.then((result) => {
1748
+ this.enabled = true;
1749
+ return result;
1750
+ }, (error) => {
1751
+ this.enabling = void 0;
1752
+ throw error;
1753
+ });
1754
+ }
1755
+ if (options?.timeout === void 0) return this.enabling;
1756
+ return new Promise((resolve, reject) => {
1757
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out enabling CDP within ${options.timeout}ms`)), options.timeout);
1758
+ this.enabling.then(resolve, reject).finally(() => clearTimeout(timer));
1759
+ });
1760
+ }
1761
+ /** 日志显式刷新时重新注册事件通道,兼容 AppService 重载。 */
1762
+ refresh(options) {
1763
+ if (this.enabled) {
1764
+ this.enabled = false;
1765
+ this.enabling = void 0;
1766
+ }
1767
+ return this.enable(options);
1768
+ }
1769
+ stop = () => {
1770
+ this.stopped = true;
1771
+ this.enabled = false;
1772
+ this.enabling = void 0;
1773
+ this.connection.off("close", this.stop);
1774
+ for (const unsubscribe of this.subscriptions) unsubscribe();
1775
+ };
1776
+ async request(method, params, options) {
1777
+ this.validateTimeout(options);
1778
+ const startedAt = Date.now();
1779
+ await this.enable(options);
1780
+ if (this.stopped) throw new Error("CDP connection closed");
1781
+ if (options?.timeout === void 0) return await this.send(method, params);
1782
+ const timeout = options.timeout - (Date.now() - startedAt);
1783
+ if (timeout <= 0) throw new Error(`Timed out executing ${method} within ${options.timeout}ms`);
1784
+ return await this.send(method, params, { timeout });
1785
+ }
1786
+ validateTimeout(options) {
1787
+ if (options?.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout <= 0)) throw new RangeError("CDP timeout must be finite and positive");
1788
+ }
1789
+ onEvent(listener) {
1790
+ if (this.stopped) throw new Error("CDP connection closed");
1791
+ const receive = (payload) => {
1792
+ listener(payload.domain, payload.event, payload.params);
1793
+ };
1794
+ const unsubscribe = () => {
1795
+ this.connection.off("App.CDPEvent", receive);
1796
+ this.subscriptions.delete(unsubscribe);
1797
+ };
1798
+ this.subscriptions.add(unsubscribe);
1799
+ this.connection.on("App.CDPEvent", receive);
1800
+ return unsubscribe;
1801
+ }
1802
+ };
1803
+ const controllers = /* @__PURE__ */ new WeakMap();
1804
+ /** 一个协议连接仅拥有一份 CDP 状态,包括被不同 MiniProgram 包装的连接。 */
1805
+ function getCDPController(connection, send) {
1806
+ let controller = controllers.get(connection);
1807
+ if (!controller) {
1808
+ controller = new CDPController(connection, send);
1809
+ controllers.set(connection, controller);
1810
+ }
1811
+ return controller;
1812
+ }
1111
1813
  //#endregion
1112
1814
  //#region src/Native.ts
1113
1815
  /** Native 的实现。 */
@@ -1158,6 +1860,25 @@ var Native = class extends EventEmitter {
1158
1860
  }
1159
1861
  };
1160
1862
  //#endregion
1863
+ //#region src/navigation/query.ts
1864
+ /** 仅核对请求显式携带的参数,保留宿主附加参数及其原始编码形式。 */
1865
+ function matchesRouteQuery(actual, expected) {
1866
+ const expectedKeys = Object.keys(expected);
1867
+ if (!expectedKeys.length) return true;
1868
+ if (!actual || typeof actual !== "object" || Array.isArray(actual)) return false;
1869
+ const query = actual;
1870
+ return expectedKeys.every((key) => {
1871
+ if (!Object.prototype.hasOwnProperty.call(query, key) || query[key] == null) return false;
1872
+ const value = String(query[key]);
1873
+ if (value === expected[key]) return true;
1874
+ try {
1875
+ return decodeURIComponent(value) === expected[key];
1876
+ } catch {
1877
+ return false;
1878
+ }
1879
+ });
1880
+ }
1881
+ //#endregion
1161
1882
  //#region src/pageRouteFallback.ts
1162
1883
  function normalizeFallbackIdPart(value) {
1163
1884
  return encodeURIComponent(value).replace(/%/g, "_");
@@ -2422,7 +3143,7 @@ function isPluginNavigationUrl(value) {
2422
3143
  }
2423
3144
  function matchesRouteChange(options) {
2424
3145
  const normalizedCurrentPath = normalizeRoutePath(options.currentPath);
2425
- if (!options.expectedRoute || normalizedCurrentPath === options.expectedRoute) return true;
3146
+ if (!options.expectedRoute || normalizedCurrentPath === options.expectedRoute) return matchesRouteQuery(options.currentQuery, options.expectedQuery);
2426
3147
  if (!isPluginNavigationUrl(options.requestedUrl) || !isPluginPath(options.currentPath)) return false;
2427
3148
  return normalizedCurrentPath !== normalizeRoutePath(options.previousPath);
2428
3149
  }
@@ -2456,16 +3177,21 @@ var MiniProgram = class extends EventEmitter {
2456
3177
  logEnabling;
2457
3178
  structuredLogs = false;
2458
3179
  structuredConsole;
3180
+ cdp;
2459
3181
  pageMap = /* @__PURE__ */ new Map();
2460
3182
  nativeIns;
3183
+ disconnecting;
3184
+ closing;
2461
3185
  constructor(connection) {
2462
3186
  super();
2463
3187
  this.connection = connection;
3188
+ this.cdp = getCDPController(connection, (method, params, options) => this.send(method, params, options));
2464
3189
  this.structuredConsole = new StructuredConsole((method, params, options) => this.send(method, params, options), (entry) => this.emit("console", entry));
2465
3190
  connection.on("App.CDPEvent", this.structuredConsole.receive);
2466
3191
  connection.on("App.logAdded", this.onLogAdded);
2467
3192
  connection.on("App.bindingCalled", this.onBindingCalled);
2468
3193
  connection.on("App.exceptionThrown", this.onExceptionThrown);
3194
+ connection.on("close", this.stopSubscriptions);
2469
3195
  }
2470
3196
  async pageStack() {
2471
3197
  const { pageStack } = await this.send("App.getPageStack");
@@ -2600,22 +3326,34 @@ var MiniProgram = class extends EventEmitter {
2600
3326
  }, sendOptions);
2601
3327
  return result;
2602
3328
  }
3329
+ /** 探测并读取 AppService JS 堆;明确不支持时返回原因,连接故障与无效响应继续抛出。 */
3330
+ async getAppServiceHeapUsage(options = {}) {
3331
+ return await readAppServiceHeapUsage((method, params, sendOptions) => this.send(method, params, sendOptions), options);
3332
+ }
2603
3333
  async pageScrollTo(scrollTop) {
2604
3334
  await this.callWxMethod("pageScrollTo", {
2605
3335
  scrollTop,
2606
3336
  duration: 0
2607
3337
  });
2608
3338
  }
2609
- async close() {
2610
- try {
2611
- await withTimeout(this.send("App.exit"), CLOSE_STEP_TIMEOUT);
2612
- } catch {}
2613
- await sleep(1e3);
2614
- try {
2615
- await withTimeout(this.send("Tool.close"), CLOSE_STEP_TIMEOUT);
2616
- } catch {} finally {
2617
- this.disconnect();
2618
- }
3339
+ close() {
3340
+ this.closing ??= (async () => {
3341
+ try {
3342
+ try {
3343
+ await withTimeout(this.send("App.exit"), CLOSE_STEP_TIMEOUT);
3344
+ } catch {}
3345
+ await sleep(1e3);
3346
+ try {
3347
+ await withTimeout(this.send("Tool.close"), CLOSE_STEP_TIMEOUT);
3348
+ } catch {}
3349
+ } finally {
3350
+ await this.disconnect();
3351
+ }
3352
+ })().catch((error) => {
3353
+ this.closing = void 0;
3354
+ throw error;
3355
+ });
3356
+ return this.closing;
2619
3357
  }
2620
3358
  async remote(auto = false) {
2621
3359
  const { qrCode } = await this.send("Tool.enableRemoteDebug", { auto });
@@ -2628,9 +3366,23 @@ var MiniProgram = class extends EventEmitter {
2628
3366
  });
2629
3367
  }
2630
3368
  disconnect() {
2631
- this.structuredConsole.stop();
2632
- this.connection.dispose();
3369
+ if (this.disconnecting) return this.disconnecting;
3370
+ this.stopSubscriptions();
3371
+ this.disconnecting = Promise.resolve(this.connection.dispose()).catch((error) => {
3372
+ this.disconnecting = void 0;
3373
+ throw error;
3374
+ });
3375
+ return this.disconnecting;
2633
3376
  }
3377
+ stopSubscriptions = () => {
3378
+ this.structuredConsole.stop();
3379
+ this.cdp.stop();
3380
+ this.connection.off("App.CDPEvent", this.structuredConsole.receive);
3381
+ this.connection.off("App.logAdded", this.onLogAdded);
3382
+ this.connection.off("App.bindingCalled", this.onBindingCalled);
3383
+ this.connection.off("App.exceptionThrown", this.onExceptionThrown);
3384
+ this.connection.off("close", this.stopSubscriptions);
3385
+ };
2634
3386
  /** 等待已接收的结构化日志完成 Error 属性检查并发布。 */
2635
3387
  async flushConsole() {
2636
3388
  await this.structuredConsole.flush();
@@ -2655,7 +3407,12 @@ var MiniProgram = class extends EventEmitter {
2655
3407
  let cdpMethod = this.structuredLogs ? "App.CDPEnable" : "App.CDPCommand";
2656
3408
  try {
2657
3409
  if (this.structuredLogs) {
2658
- await request(cdpMethod);
3410
+ if (timeout === void 0) await this.cdp.refresh();
3411
+ else {
3412
+ const remaining = timeout - (Date.now() - startedAt);
3413
+ if (remaining <= 0) throw new Error(`Timed out enabling console logging within ${timeout}ms`);
3414
+ await this.cdp.refresh({ timeout: remaining });
3415
+ }
2659
3416
  cdpMethod = "App.CDPCommand";
2660
3417
  }
2661
3418
  await request("App.CDPCommand", {
@@ -2695,7 +3452,7 @@ var MiniProgram = class extends EventEmitter {
2695
3452
  if (sdkVersion !== "dev" && cmpVersion(sdkVersion, "2.7.3") < 0) throw new Error(`SDKVersion is currently ${sdkVersion}, while automator(${version}) requires at least version 2.7.3`);
2696
3453
  }
2697
3454
  /**
2698
- * @description 等待小程序 App 域协议可用,避免返回半就绪自动化会话。
3455
+ * @description 等待原生协议返回真实页面标识和路径,避免返回半就绪自动化会话。
2699
3456
  */
2700
3457
  async waitForAppReady(timeout = APP_READY_TIMEOUT) {
2701
3458
  const startedAt = Date.now();
@@ -2703,13 +3460,15 @@ var MiniProgram = class extends EventEmitter {
2703
3460
  while (Date.now() - startedAt < timeout) {
2704
3461
  const remainingTimeout = timeout - (Date.now() - startedAt);
2705
3462
  try {
2706
- await this.send("App.captureScreenshot", {}, { timeout: Math.min(APP_READY_PROBE_TIMEOUT, remainingTimeout) });
2707
- return;
3463
+ const page = await this.send("App.getCurrentPage", {}, { timeout: Math.min(APP_READY_PROBE_TIMEOUT, remainingTimeout) });
3464
+ if (page && typeof page === "object" && "pageId" in page && typeof page.pageId === "number" && Number.isSafeInteger(page.pageId) && page.pageId >= 0 && "path" in page && typeof page.path === "string" && page.path.trim()) return;
3465
+ lastError = /* @__PURE__ */ new Error("App.getCurrentPage returned no ready page with a valid pageId and path");
2708
3466
  } catch (error) {
3467
+ if (!isCurrentPageProtocolTimeout(error) && !isPageMetaMissingError(error) && !isCurrentFrameTimedOutError(error)) throw error;
2709
3468
  lastError = error;
2710
- const remainingAfterProbe = timeout - (Date.now() - startedAt);
2711
- if (remainingAfterProbe > 0) await sleep(Math.min(APP_READY_POLL_DELAY, remainingAfterProbe));
2712
3469
  }
3470
+ const remainingAfterProbe = timeout - (Date.now() - startedAt);
3471
+ if (remainingAfterProbe > 0) await sleep(Math.min(APP_READY_POLL_DELAY, remainingAfterProbe));
2713
3472
  }
2714
3473
  throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Timed out waiting App domain readiness after ${timeout}ms`);
2715
3474
  }
@@ -2800,9 +3559,11 @@ var MiniProgram = class extends EventEmitter {
2800
3559
  logChangeRouteDebug(`poll method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
2801
3560
  if (matchesRouteChange({
2802
3561
  currentPath: page?.path,
3562
+ currentQuery: page?.query,
2803
3563
  previousPath: currentPage?.path,
2804
3564
  requestedUrl: url,
2805
- expectedRoute
3565
+ expectedRoute,
3566
+ expectedQuery: expectedRouteQuery
2806
3567
  })) {
2807
3568
  logChangeRouteDebug(`ready method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
2808
3569
  return page;
@@ -2819,9 +3580,11 @@ var MiniProgram = class extends EventEmitter {
2819
3580
  logChangeRouteDebug(`stack method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
2820
3581
  if (matchesRouteChange({
2821
3582
  currentPath: stackTop.path,
3583
+ currentQuery: stackTop.query,
2822
3584
  previousPath: currentPage?.path,
2823
3585
  requestedUrl: url,
2824
- expectedRoute
3586
+ expectedRoute,
3587
+ expectedQuery: expectedRouteQuery
2825
3588
  })) {
2826
3589
  logChangeRouteDebug(`stack-ready method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
2827
3590
  return stackTop;
@@ -2917,6 +3680,11 @@ var MiniProgram = class extends EventEmitter {
2917
3680
  }
2918
3681
  throw lastError;
2919
3682
  }
3683
+ /** 返回当前连接共享的 CDP 调试入口。 */
3684
+ CDP() {
3685
+ return this.cdp.session;
3686
+ }
3687
+ /** 发送原始自动化协议,沿用连接的请求超时及清理契约。 */
2920
3688
  async send(method, params = {}, options) {
2921
3689
  return options ? await this.connection.send(method, params, options) : await this.connection.send(method, params);
2922
3690
  }
@@ -3120,7 +3888,7 @@ var SmartappApp = class {
3120
3888
  return [];
3121
3889
  }
3122
3890
  async close() {
3123
- this.miniProgram?.disconnect();
3891
+ await this.miniProgram?.disconnect();
3124
3892
  }
3125
3893
  };
3126
3894
  //#endregion
@@ -3198,8 +3966,8 @@ var SmartappDevice = class {
3198
3966
  return false;
3199
3967
  }
3200
3968
  async close() {
3201
- await this.driver.close?.();
3202
- this.driver.miniProgram?.disconnect();
3969
+ const errors = (await Promise.allSettled([Promise.resolve().then(() => this.driver.close?.()), Promise.resolve().then(() => this.driver.miniProgram?.disconnect())])).filter((result) => result.status === "rejected").map((result) => result.reason);
3970
+ if (errors.length) throw new AggregateError(errors, "Smartapp device cleanup failed");
3203
3971
  }
3204
3972
  };
3205
3973
  //#endregion
@@ -3364,40 +4132,36 @@ const VERSION_CHECK_TIMEOUT = 3e4;
3364
4132
  const AUTOMATOR_LAUNCH_RETRIES = 3;
3365
4133
  const DEFAULT_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER";
3366
4134
  const LEGACY_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_E2E_RUNTIME_PROVIDER";
3367
- const EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
3368
- const RETRYABLE_LAUNCH_PORT_RE = /Wait timed out after \d+ ms|Failed connecting to ws:\/\/127\.0\.0\.1:\d+|Failed connecting to devtools websocket endpoint|Failed to launch wechat web devTools, please make sure cliPath is correctly specified/i;
3369
- const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
3370
4135
  let localhostListenPatched = false;
3371
- function isExtensionContextInvalidatedError(error) {
3372
- return error instanceof Error && EXTENSION_CONTEXT_INVALIDATED_RE.test(error.message);
3373
- }
3374
- function isRetryableAutomatorPortLaunchError(error) {
3375
- return error instanceof Error && RETRYABLE_LAUNCH_PORT_RE.test(error.message);
3376
- }
3377
4136
  function retainPortLeaseUntilSessionClose(miniProgram, portLease) {
3378
- let released = false;
4137
+ let released;
3379
4138
  const release = async () => {
3380
- if (released) return;
3381
- released = true;
3382
- await portLease.release();
4139
+ released ??= portLease.release().catch((error) => {
4140
+ released = void 0;
4141
+ throw error;
4142
+ });
4143
+ await released;
3383
4144
  };
3384
4145
  const target = miniProgram;
3385
4146
  const rawClose = target.close;
3386
4147
  const rawDisconnect = target.disconnect;
3387
4148
  if (typeof rawClose !== "function" && typeof rawDisconnect !== "function") return false;
3388
- if (typeof rawDisconnect === "function") target.disconnect = function disconnectWithPortLeaseRelease() {
3389
- try {
3390
- return rawDisconnect.call(this);
3391
- } finally {
3392
- release();
3393
- }
3394
- };
4149
+ if (typeof rawDisconnect === "function") {
4150
+ let disconnecting;
4151
+ target.disconnect = function disconnectWithPortLeaseRelease() {
4152
+ disconnecting ??= (async () => {
4153
+ await rawDisconnect.call(this);
4154
+ await release();
4155
+ })().catch((error) => {
4156
+ disconnecting = void 0;
4157
+ throw error;
4158
+ });
4159
+ return disconnecting;
4160
+ };
4161
+ }
3395
4162
  if (typeof rawClose === "function") target.close = async function closeWithPortLeaseRelease() {
3396
- try {
3397
- return await rawClose.call(this);
3398
- } finally {
3399
- await release();
3400
- }
4163
+ await rawClose.call(this);
4164
+ await release();
3401
4165
  };
3402
4166
  return true;
3403
4167
  }
@@ -3418,25 +4182,6 @@ function patchNetListenToLoopback() {
3418
4182
  return rawListen.apply(this, args);
3419
4183
  };
3420
4184
  }
3421
- /** IConnectOptions 的类型定义。 */
3422
- function shouldUseWindowsCommandShell(cliPath) {
3423
- return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
3424
- }
3425
- function escapeWindowsCmdArg(arg) {
3426
- const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
3427
- return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
3428
- }
3429
- function resolveWindowsBatchSpawn(cliPath, args) {
3430
- return {
3431
- file: process.env.ComSpec || "cmd.exe",
3432
- args: [
3433
- "/d",
3434
- "/s",
3435
- "/c",
3436
- `"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
3437
- ]
3438
- };
3439
- }
3440
4185
  function resolveRuntimeProvider(options) {
3441
4186
  return options.runtimeProvider || process.env[DEFAULT_RUNTIME_PROVIDER_ENV] || process.env[LEGACY_RUNTIME_PROVIDER_ENV] || "devtools";
3442
4187
  }
@@ -3449,88 +4194,88 @@ var Launcher = class {
3449
4194
  return await launchHeadlessAutomator({ projectPath: options.projectPath });
3450
4195
  }
3451
4196
  patchNetListenToLoopback();
3452
- if (options.port) return await this.launchWechatDevtools(options);
3453
- let lastError = null;
3454
- for (let attempt = 1; attempt <= AUTOMATOR_LAUNCH_RETRIES; attempt += 1) try {
3455
- return await this.launchWechatDevtools(options);
3456
- } catch (error) {
3457
- lastError = error;
3458
- if (!isRetryableAutomatorPortLaunchError(error) || attempt === AUTOMATOR_LAUNCH_RETRIES) throw error;
3459
- }
3460
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
4197
+ return await new OperationLifecycle(options.timeout ?? DEFAULT_TIMEOUT, "automator launch", options.signal).run(async (scope) => {
4198
+ const attempts = options.port ? 1 : AUTOMATOR_LAUNCH_RETRIES;
4199
+ for (let attempt = 1;; attempt += 1) {
4200
+ scope.attempt();
4201
+ try {
4202
+ return await scope.step(() => this.launchWechatDevtools(options, scope));
4203
+ } catch (error) {
4204
+ scope.recordFailure(error);
4205
+ scope.throwIfAborted();
4206
+ if (error instanceof AggregateError || !isRecoverableOperationError(error) || attempt >= attempts) throw error;
4207
+ }
4208
+ }
4209
+ });
3461
4210
  }
3462
- async launchWechatDevtools(options) {
3463
- const { cliPath = await this.resolveCliPath(), timeout = DEFAULT_TIMEOUT, projectConfig = {}, ticket = "", cwd = "", account = "", trustProject = false } = options;
3464
- let { args = [], projectPath } = options;
3465
- const portLease = await acquireAutomatorPortLease(options.port);
4211
+ async launchWechatDevtools(options, scope) {
4212
+ const { cliPath = await scope.step(() => this.resolveCliPath(), { stage: "resolve-cli" }), projectConfig = {}, ticket = "", cwd = "", account = "", trustProject = false } = options;
4213
+ const { args = [], projectPath } = options;
4214
+ const portLease = await scope.step(() => acquireAutomatorPortLease(options.port), {
4215
+ stage: "port-lease",
4216
+ disposeLate: (lease) => lease.release()
4217
+ });
4218
+ const rawReleaseLease = portLease.release.bind(portLease);
4219
+ let releasedLease;
4220
+ portLease.release = () => releasedLease ??= rawReleaseLease().catch((error) => {
4221
+ releasedLease = void 0;
4222
+ throw error;
4223
+ });
4224
+ const connectionTasks = [];
4225
+ const disconnectingConnections = /* @__PURE__ */ new WeakMap();
4226
+ const disconnectConnection = (program) => {
4227
+ const pending = disconnectingConnections.get(program);
4228
+ if (pending) return pending;
4229
+ const closing = Promise.resolve(program.disconnect?.()).catch((error) => {
4230
+ disconnectingConnections.delete(program);
4231
+ throw error;
4232
+ });
4233
+ disconnectingConnections.set(program, closing);
4234
+ return closing;
4235
+ };
4236
+ const releaseConnectionsAndLease = async () => {
4237
+ const results = await Promise.allSettled(connectionTasks);
4238
+ const errors = (await Promise.allSettled(results.map((result) => result.status === "fulfilled" ? disconnectConnection(result.value) : Promise.resolve()))).filter((result) => result.status === "rejected").map((result) => result.reason);
4239
+ if (errors.length) throw new AggregateError(errors, "Automator connections did not finish closing; port lease retained");
4240
+ await portLease.release();
4241
+ };
4242
+ const disownLease = scope.own(releaseConnectionsAndLease, "port-lease");
3466
4243
  let releasePortLeaseOnExit = true;
4244
+ let releaseChild = async () => {};
4245
+ let diagnoseFailure = (error) => error;
4246
+ let failure;
4247
+ let failed = false;
4248
+ let result;
3467
4249
  try {
3468
4250
  const port = portLease.port;
3469
4251
  if (!cliPath) throw new Error("Wechat web devTools not found, please specify cliPath option");
3470
4252
  if (isWindows && endWith(cliPath, ".exe")) throw new Error("cliPath is not correct, it's usually named as 'cli' or 'cli.bat'");
3471
4253
  if (!projectPath) throw new Error("projectPath is not provided");
3472
4254
  const resolvedProjectPath = isRelative(projectPath) ? path.resolve(projectPath) : projectPath;
3473
- if (isRelative(projectPath)) projectPath = resolvedProjectPath;
3474
- if (!await import("node:fs/promises").then((fs) => fs.access(resolvedProjectPath).then(() => true).catch(() => false))) throw new Error(`Project path ${resolvedProjectPath} doesn't exist`);
3475
- if (!isEmpty(projectConfig)) await this.extendProjectConfig(projectConfig, resolvedProjectPath);
3476
- let processError = null;
3477
- let processExitCode = null;
3478
- let processSignal = null;
3479
- let successfulCliExitSettled = false;
4255
+ if (!await scope.step(() => import("node:fs/promises").then((fs) => fs.access(resolvedProjectPath).then(() => true).catch(() => false)), { stage: "project" })) throw new Error(`Project path ${resolvedProjectPath} doesn't exist`);
4256
+ if (!isEmpty(projectConfig)) await scope.step(() => this.extendProjectConfig(projectConfig, resolvedProjectPath), { stage: "project-config" });
4257
+ scope.throwIfAborted();
3480
4258
  let httpFallbackAttempted = false;
3481
4259
  let httpFallbackError = null;
3482
4260
  let targetPort = port;
3483
- const cliOutput = [];
3484
- args = resolveWechatDevtoolsBootstrapArgs(args);
3485
- try {
3486
- const spawnTarget = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, args) : {
3487
- file: cliPath,
3488
- args
3489
- };
3490
- const child = spawn(spawnTarget.file, spawnTarget.args, {
3491
- stdio: [
3492
- "ignore",
3493
- "pipe",
3494
- "pipe"
3495
- ],
3496
- cwd: cwd || void 0,
3497
- ...shouldUseWindowsCommandShell(cliPath) ? {
3498
- windowsHide: true,
3499
- windowsVerbatimArguments: true
3500
- } : {}
3501
- });
3502
- child.on("error", (error) => {
3503
- processError = error;
3504
- });
3505
- child.stdout?.on("data", (chunk) => {
3506
- cliOutput.push(String(chunk));
3507
- });
3508
- child.stderr?.on("data", (chunk) => {
3509
- cliOutput.push(String(chunk));
3510
- });
3511
- child.on("exit", (code, signal) => {
3512
- processExitCode = code;
3513
- processSignal = signal;
3514
- if (code !== 0 || signal) processError = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
3515
- else successfulCliExitSettled = true;
3516
- });
3517
- child.unref();
3518
- } catch (error) {
3519
- processError = error;
3520
- }
4261
+ const cli = spawnWechatCli(cliPath, args, cwd, scope);
4262
+ diagnoseFailure = (error) => attachCliDiagnostics(error, cli, [ticket, account]);
4263
+ releaseChild = cli.release;
3521
4264
  let miniProgram = null;
3522
4265
  let lastConnectError = null;
3523
- const readinessStartedAt = Date.now();
3524
- const resolveRemainingTimeout = () => Math.max(0, timeout - (Date.now() - readinessStartedAt));
3525
- await waitUntil(async () => {
4266
+ const resolveRemainingTimeout = () => scope.remainingMs();
4267
+ await scope.step(() => waitUntil(async () => {
4268
+ scope.throwIfAborted();
3526
4269
  try {
3527
- if (processError) return true;
3528
- if (successfulCliExitSettled && !httpFallbackAttempted) {
3529
- const servicePort = extractWechatDevtoolsServicePort(cliOutput.join("\n"));
4270
+ if (cli.error) return true;
4271
+ if (cli.success && readWechatLoginState(cli.output) === false) throw Object.assign(/* @__PURE__ */ new Error("DEVTOOLS_LOGIN_REQUIRED: need re-login"), { code: 10 });
4272
+ if (cli.success && !httpFallbackAttempted) {
4273
+ const servicePort = extractWechatDevtoolsServicePort(cli.output);
3530
4274
  if (servicePort) {
3531
4275
  httpFallbackAttempted = true;
3532
4276
  try {
3533
4277
  targetPort = await enableAutomatorViaHttp({
4278
+ signal: scope.signal,
3534
4279
  account,
3535
4280
  autoPort: port,
3536
4281
  projectPath: resolvedProjectPath,
@@ -3546,43 +4291,68 @@ var Launcher = class {
3546
4291
  }
3547
4292
  const connectTimeout = resolveRemainingTimeout();
3548
4293
  if (connectTimeout <= 0) return false;
3549
- const candidate = await this.connectTool({
3550
- timeout: Math.min(3e3, connectTimeout),
3551
- wsEndpoint: `ws://127.0.0.1:${targetPort}`
4294
+ const candidate = await scope.step(() => {
4295
+ const connecting = this.connectTool({
4296
+ signal: scope.signal,
4297
+ timeout: Math.min(3e3, connectTimeout),
4298
+ wsEndpoint: `ws://127.0.0.1:${targetPort}`
4299
+ });
4300
+ connectionTasks.push(connecting);
4301
+ return connecting;
4302
+ }, {
4303
+ stage: "websocket",
4304
+ disposeLate: disconnectConnection
3552
4305
  });
4306
+ let releasedCandidate;
4307
+ const releaseCandidate = () => {
4308
+ return releasedCandidate ??= disconnectConnection(candidate).catch((error) => {
4309
+ releasedCandidate = void 0;
4310
+ throw error;
4311
+ });
4312
+ };
4313
+ const disownCandidate = scope.own(releaseCandidate, "websocket");
3553
4314
  try {
3554
4315
  const checkVersionTimeout = resolveRemainingTimeout();
3555
4316
  if (checkVersionTimeout <= 0) {
3556
- candidate.disconnect();
4317
+ await scope.step(releaseCandidate);
3557
4318
  return false;
3558
4319
  }
3559
- await candidate.checkVersion(Math.min(VERSION_CHECK_TIMEOUT, checkVersionTimeout));
4320
+ await scope.step(() => candidate.checkVersion(Math.min(VERSION_CHECK_TIMEOUT, checkVersionTimeout)), { stage: "version" });
3560
4321
  if (typeof candidate.waitForAppReady === "function") {
3561
4322
  const appReadyTimeout = resolveRemainingTimeout();
3562
4323
  if (appReadyTimeout <= 0) {
3563
- candidate.disconnect();
4324
+ await scope.step(releaseCandidate);
3564
4325
  return false;
3565
4326
  }
3566
- await candidate.waitForAppReady(appReadyTimeout);
4327
+ await scope.step(() => candidate.waitForAppReady(appReadyTimeout), { stage: "app-ready" });
3567
4328
  }
3568
4329
  } catch (error) {
3569
- candidate.disconnect();
4330
+ try {
4331
+ await scope.step(releaseCandidate);
4332
+ } catch (cleanupError) {
4333
+ if (cleanupError !== error) throw new AggregateError([error, cleanupError], "Automator validation and connection cleanup failed", { cause: error });
4334
+ throw error;
4335
+ }
4336
+ disownCandidate();
3570
4337
  lastConnectError = error;
3571
- if (isExtensionContextInvalidatedError(error)) return false;
4338
+ if (isRecoverableOperationError(error)) return false;
3572
4339
  throw error;
3573
4340
  }
3574
4341
  miniProgram = candidate;
3575
4342
  return true;
3576
4343
  } catch (error) {
3577
4344
  lastConnectError = error;
4345
+ scope.recordFailure(error);
4346
+ scope.throwIfAborted();
4347
+ if (error instanceof AggregateError || !isRecoverableOperationError(error)) throw error;
3578
4348
  return false;
3579
4349
  }
3580
- }, timeout, 1e3);
4350
+ }, scope.remainingMs(), 1e3, scope.signal), { stage: "readiness" });
3581
4351
  if (!miniProgram) {
3582
4352
  if (httpFallbackError) throw httpFallbackError;
3583
- if (processError) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified");
4353
+ if (cli.error) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified", { cause: cli.error });
3584
4354
  if (lastConnectError) throw lastConnectError;
3585
- if (processExitCode !== null || processSignal) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
4355
+ if (cli.exited) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
3586
4356
  throw new Error("Failed connecting to devtools websocket endpoint");
3587
4357
  }
3588
4358
  const resolvedMiniProgram = miniProgram;
@@ -3592,33 +4362,47 @@ var Launcher = class {
3592
4362
  wsEndpoint: `ws://127.0.0.1:${targetPort}`
3593
4363
  });
3594
4364
  releasePortLeaseOnExit = !retainPortLeaseUntilSessionClose(resolvedMiniProgram, portLease);
3595
- await sleep$1(5e3);
3596
- return resolvedMiniProgram;
3597
- } finally {
3598
- if (releasePortLeaseOnExit) await portLease.release();
4365
+ if (!releasePortLeaseOnExit) disownLease();
4366
+ result = resolvedMiniProgram;
4367
+ } catch (error) {
4368
+ failed = true;
4369
+ failure = diagnoseFailure(error);
3599
4370
  }
4371
+ const cleanupErrors = [];
4372
+ try {
4373
+ await scope.step(releaseChild);
4374
+ } catch (error) {
4375
+ if (!failed || error !== failure) cleanupErrors.push(error);
4376
+ }
4377
+ if (releasePortLeaseOnExit) try {
4378
+ await scope.step(releaseConnectionsAndLease);
4379
+ disownLease();
4380
+ } catch (error) {
4381
+ if ((!failed || error !== failure) && !cleanupErrors.includes(error)) cleanupErrors.push(error);
4382
+ }
4383
+ if (cleanupErrors.length) {
4384
+ if (failed) throw new AggregateError([failure, ...cleanupErrors], "Automator launch and resource cleanup failed", { cause: failure });
4385
+ throw new AggregateError(cleanupErrors, "Automator launch resource cleanup failed");
4386
+ }
4387
+ if (failed) throw failure;
4388
+ return result;
3600
4389
  }
3601
4390
  async connect(options) {
3602
4391
  if (normalizePlatform(options.platform) === "swan") return await new SwanLauncher().connect(options);
3603
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
3604
- const deadlineAt = performance.now() + timeout;
3605
- const miniProgram = await this.connectTool({
3606
- ...options,
3607
- timeout
3608
- });
3609
- try {
3610
- const remaining = Math.ceil(deadlineAt - performance.now());
3611
- if (remaining <= 0) throw new Error(`Timed out connecting to automator after ${timeout}ms`);
3612
- await miniProgram.checkVersion(remaining);
4392
+ return await new OperationLifecycle(options.timeout ?? DEFAULT_TIMEOUT, "automator connect", options.signal).run(async (scope) => {
4393
+ scope.attempt();
4394
+ const miniProgram = await scope.step(() => this.connectTool({
4395
+ ...options,
4396
+ signal: scope.signal,
4397
+ timeout: scope.remainingMs()
4398
+ }), {
4399
+ stage: "websocket",
4400
+ disposeLate: (program) => program.disconnect()
4401
+ });
4402
+ scope.own(() => miniProgram.disconnect(), "websocket");
4403
+ await scope.step(() => miniProgram.checkVersion(scope.remainingMs()), { stage: "version" });
3613
4404
  return miniProgram;
3614
- } catch (error) {
3615
- try {
3616
- miniProgram.disconnect();
3617
- } catch (cleanupError) {
3618
- throw new AggregateError([error, cleanupError], "Automator connection and cleanup both failed", { cause: error });
3619
- }
3620
- throw error;
3621
- }
4405
+ });
3622
4406
  }
3623
4407
  async extendProjectConfig(projectConfig, projectPath) {
3624
4408
  const projectConfigPath = path.resolve(projectPath, "project.config.json");
@@ -3630,9 +4414,9 @@ var Launcher = class {
3630
4414
  }
3631
4415
  async connectTool(options) {
3632
4416
  try {
3633
- return new MiniProgram(await Connection.create(options.wsEndpoint, options.timeout));
3634
- } catch {
3635
- throw new Error(`Failed connecting to ${options.wsEndpoint}, check if target project window is opened with automation enabled`);
4417
+ return new MiniProgram(await Connection.create(options.wsEndpoint, options.timeout, options.signal));
4418
+ } catch (cause) {
4419
+ throw new Error(`Failed connecting to ${options.wsEndpoint}, check if target project window is opened with automation enabled`, { cause });
3636
4420
  }
3637
4421
  }
3638
4422
  async resolveCliPath() {
@@ -3674,4 +4458,4 @@ var Automator = class {
3674
4458
  */
3675
4459
  const SmartappAutomator = smartappAutomatorRuntime;
3676
4460
  //#endregion
3677
- export { Automator, Connection, ContextElement, CustomElement, Element, InputElement, Launcher, MiniProgram, MovableViewElement, Native, Page, ScrollViewElement, SliderElement, SmartappAutomator, SwiperElement, SwitchElement, TextareaElement, Transport, decodeQrCode, extractPluginId, isPluginPath, normalizePlatform, printQrCode };
4461
+ export { Automator, Connection, ContextElement, CustomElement, Element, InputElement, Launcher, MiniProgram, MovableViewElement, Native, OPERATION_TIMEOUT_CODE, OperationLifecycle, Page, ScrollViewElement, SliderElement, SmartappAutomator, SwiperElement, SwitchElement, TextareaElement, Transport, acquireAutomatorPortLease, classifyOperationError, decodeQrCode, extractPluginId, isPluginPath, isRecoverableOperationError, normalizePlatform, printQrCode, readWechatLoginState };