@midscene/web 1.9.8 → 1.10.1-beta-20260624112700.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.
Files changed (84) hide show
  1. package/dist/es/{mcp-agent-init-args.mjs → agent-init-args.mjs} +2 -2
  2. package/dist/es/{mcp-agent-init-args.mjs.map → agent-init-args.mjs.map} +1 -1
  3. package/dist/es/{mcp-tools-cdp.mjs → agent-tools-cdp.mjs} +5 -5
  4. package/dist/es/agent-tools-cdp.mjs.map +1 -0
  5. package/dist/es/{mcp-tools-puppeteer.mjs → agent-tools-puppeteer.mjs} +7 -7
  6. package/dist/es/agent-tools-puppeteer.mjs.map +1 -0
  7. package/dist/es/{mcp-tools.mjs → agent-tools.mjs} +4 -4
  8. package/dist/es/agent-tools.mjs.map +1 -0
  9. package/dist/es/bridge-mode/io-client.mjs +1 -1
  10. package/dist/es/bridge-mode/io-server.mjs +2 -2
  11. package/dist/es/bridge-mode/io-server.mjs.map +1 -1
  12. package/dist/es/bridge-mode/page-browser-side.mjs +1 -1
  13. package/dist/es/bridge-mode/page-browser-side.mjs.map +1 -1
  14. package/dist/es/cdp-proxy-constants.mjs.map +1 -1
  15. package/dist/es/cdp-proxy-manager.mjs +1 -1
  16. package/dist/es/cdp-proxy-manager.mjs.map +1 -1
  17. package/dist/es/cdp-proxy.mjs.map +1 -1
  18. package/dist/es/cdp-target-store.mjs +1 -1
  19. package/dist/es/cdp-target-store.mjs.map +1 -1
  20. package/dist/es/cli.mjs +4 -4
  21. package/dist/es/cli.mjs.map +1 -1
  22. package/dist/es/index.mjs +2 -2
  23. package/dist/es/playwright/index.mjs +5 -3
  24. package/dist/es/playwright/index.mjs.map +1 -1
  25. package/dist/es/puppeteer/base-page.mjs +43 -44
  26. package/dist/es/puppeteer/base-page.mjs.map +1 -1
  27. package/dist/es/puppeteer/index.mjs +5 -3
  28. package/dist/es/puppeteer/index.mjs.map +1 -1
  29. package/dist/es/static/static-page.mjs +8 -3
  30. package/dist/es/static/static-page.mjs.map +1 -1
  31. package/dist/es/web-element.mjs.map +1 -1
  32. package/dist/lib/{mcp-agent-init-args.js → agent-init-args.js} +2 -2
  33. package/dist/lib/{mcp-agent-init-args.js.map → agent-init-args.js.map} +1 -1
  34. package/dist/lib/{mcp-tools-cdp.js → agent-tools-cdp.js} +7 -7
  35. package/dist/lib/agent-tools-cdp.js.map +1 -0
  36. package/dist/lib/{mcp-tools-puppeteer.js → agent-tools-puppeteer.js} +9 -9
  37. package/dist/lib/agent-tools-puppeteer.js.map +1 -0
  38. package/dist/lib/{mcp-tools.js → agent-tools.js} +6 -6
  39. package/dist/lib/agent-tools.js.map +1 -0
  40. package/dist/lib/bridge-mode/io-client.js +1 -1
  41. package/dist/lib/bridge-mode/io-server.js +2 -2
  42. package/dist/lib/bridge-mode/io-server.js.map +1 -1
  43. package/dist/lib/bridge-mode/page-browser-side.js +1 -1
  44. package/dist/lib/bridge-mode/page-browser-side.js.map +1 -1
  45. package/dist/lib/cdp-proxy-constants.js.map +1 -1
  46. package/dist/lib/cdp-proxy-manager.js +1 -1
  47. package/dist/lib/cdp-proxy-manager.js.map +1 -1
  48. package/dist/lib/cdp-proxy.js.map +1 -1
  49. package/dist/lib/cdp-target-store.js +1 -1
  50. package/dist/lib/cdp-target-store.js.map +1 -1
  51. package/dist/lib/cli.js +5 -5
  52. package/dist/lib/cli.js.map +1 -1
  53. package/dist/lib/index.js +4 -4
  54. package/dist/lib/playwright/index.js +5 -3
  55. package/dist/lib/playwright/index.js.map +1 -1
  56. package/dist/lib/puppeteer/base-page.js +43 -44
  57. package/dist/lib/puppeteer/base-page.js.map +1 -1
  58. package/dist/lib/puppeteer/index.js +5 -3
  59. package/dist/lib/puppeteer/index.js.map +1 -1
  60. package/dist/lib/static/static-page.js +8 -3
  61. package/dist/lib/static/static-page.js.map +1 -1
  62. package/dist/lib/web-element.js.map +1 -1
  63. package/dist/types/{mcp-agent-init-args.d.ts → agent-init-args.d.ts} +1 -1
  64. package/dist/types/{mcp-tools-cdp.d.ts → agent-tools-cdp.d.ts} +4 -4
  65. package/dist/types/{mcp-tools-puppeteer.d.ts → agent-tools-puppeteer.d.ts} +4 -4
  66. package/dist/types/{mcp-tools.d.ts → agent-tools.d.ts} +4 -4
  67. package/dist/types/cdp-proxy-manager.d.ts +1 -1
  68. package/dist/types/cdp-proxy.d.ts +1 -1
  69. package/dist/types/index.d.ts +2 -2
  70. package/dist/types/puppeteer/base-page.d.ts +2 -9
  71. package/dist/types/static/static-page.d.ts +9 -2
  72. package/dist/types/web-element.d.ts +3 -0
  73. package/package.json +4 -9
  74. package/dist/es/mcp-server.mjs +0 -35
  75. package/dist/es/mcp-server.mjs.map +0 -1
  76. package/dist/es/mcp-tools-cdp.mjs.map +0 -1
  77. package/dist/es/mcp-tools-puppeteer.mjs.map +0 -1
  78. package/dist/es/mcp-tools.mjs.map +0 -1
  79. package/dist/lib/mcp-server.js +0 -75
  80. package/dist/lib/mcp-server.js.map +0 -1
  81. package/dist/lib/mcp-tools-cdp.js.map +0 -1
  82. package/dist/lib/mcp-tools-puppeteer.js.map +0 -1
  83. package/dist/lib/mcp-tools.js.map +0 -1
  84. package/dist/types/mcp-server.d.ts +0 -26
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-proxy.mjs","sources":["../../src/cdp-proxy.ts"],"sourcesContent":["/**\n * CDP WebSocket Proxy — standalone process.\n *\n * Holds a persistent WebSocket connection to Chrome's CDP endpoint and\n * exposes a local WebSocket server. Midscene CLI processes connect to the\n * proxy instead of Chrome directly, so Chrome's \"Allow remote debugging\"\n * permission popup only fires once (when the proxy connects).\n *\n * Lifecycle notes:\n * - When all downstream clients disconnect the proxy stays running but\n * marks the upstream as needing reconnection. The actual reconnect is\n * deferred to the moment the next client connects, so Chrome's CDP\n * state (notably Target.setDiscoverTargets) is reset and the new\n * client receives all targetCreated events.\n * - On startup, if another proxy is already alive the new instance\n * announces \"duplicate proxy detected\" on stderr and exits 0 without\n * touching the existing metadata files.\n *\n * Exit conditions:\n * 1. Upstream Chrome connection closes or errors.\n * 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).\n * 3. SIGTERM / SIGINT.\n * 4. Duplicate proxy detected on startup (exits 0 with stderr notice).\n *\n * Usage (spawned by mcp-tools-cdp.ts):\n * node cdp-proxy.js <chrome-ws-endpoint>\n *\n * On startup, prints the proxy endpoint to stdout as a single JSON line:\n * {\"endpoint\":\"ws://127.0.0.1:<port>/devtools/browser\"}\n * and writes:\n * - PROXY_ENDPOINT_FILE — the local proxy URL above\n * - PROXY_PID_FILE — this process's pid\n * - PROXY_UPSTREAM_FILE — the Chrome endpoint the proxy is connected to,\n * so callers can detect when the requested\n * upstream has changed and replace the proxy.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n PROXY_ENDPOINT_FILE,\n PROXY_PID_FILE,\n PROXY_UPSTREAM_FILE,\n} from './cdp-proxy-constants';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\nconst chromeEndpoint = process.argv[2];\nif (!chromeEndpoint) {\n process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\\n');\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Cleanup\n// ---------------------------------------------------------------------------\n\nfunction cleanupIfOwned() {\n // Only clean up if the PID file exists and records *our* PID.\n // If the file is missing (e.g. already deleted by killProxy()) or\n // unreadable, skip cleanup — another process may have taken over.\n try {\n if (!existsSync(PROXY_PID_FILE)) return;\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (pid !== process.pid) return;\n } catch {\n return;\n }\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_UPSTREAM_FILE)) unlinkSync(PROXY_UPSTREAM_FILE);\n } catch {}\n}\n\n/**\n * Maximum time to wait for the stderr drain callback before forcing exit.\n * The callback should normally fire within microseconds, but if the pipe\n * has been closed by the parent it may never run. 500ms is generous\n * enough to be effectively unreachable in the happy path while still\n * keeping the process from hanging if something goes wrong.\n */\nconst STDERR_FLUSH_FALLBACK_MS = 500;\n\n/**\n * Exit after the stderr diagnostic has been flushed.\n *\n * When the proxy's stderr is a pipe (parent uses stdio 'pipe'), Node's\n * process.stderr.write() is asynchronous on POSIX. Calling process.exit()\n * immediately afterwards can drop the pending write, which would silently\n * lose the very diagnostic the caller is relying on. Wait for the drain\n * callback before exiting, with a short fallback timer in case the\n * callback never fires (e.g. the pipe is already closed).\n */\nfunction exitWithStderr(message: string, code = 0): void {\n let exited = false;\n const doExit = () => {\n if (exited) return;\n exited = true;\n process.exit(code);\n };\n const fallback = setTimeout(doExit, STDERR_FLUSH_FALLBACK_MS);\n fallback.unref?.();\n try {\n process.stderr.write(message, () => doExit());\n } catch {\n doExit();\n }\n}\n\nfunction shutdown(reason: string): void {\n cleanupIfOwned();\n exitWithStderr(`[cdp-proxy] shutting down: ${reason}\\n`, 0);\n}\n\nprocess.on('SIGTERM', () => shutdown('SIGTERM'));\nprocess.on('SIGINT', () => shutdown('SIGINT'));\nprocess.on('uncaughtException', (e) => shutdown(`uncaught: ${e.message}`));\n\n// ---------------------------------------------------------------------------\n// Idle timer\n// ---------------------------------------------------------------------------\n\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction resetIdleTimer() {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(\n () => shutdown('idle timeout (5min)'),\n IDLE_TIMEOUT_MS,\n );\n}\n\nresetIdleTimer();\n\n// ---------------------------------------------------------------------------\n// Upstream: connect to Chrome\n// ---------------------------------------------------------------------------\n\nconst clients = new Set<WebSocket>();\n\n/**\n * Whether we are intentionally reconnecting the upstream WebSocket.\n * When true, the old upstream's close/error events should not trigger shutdown.\n */\nlet reconnecting = false;\n\n/**\n * Whether the upstream WebSocket needs to be reconnected before the next\n * client can use it. Set to true when all downstream clients disconnect;\n * the actual reconnect is deferred until a new client connects so that\n * Chrome's permission popup only fires when someone actually needs it.\n */\nlet needsUpstreamReconnect = false;\n\n/**\n * Messages from downstream clients that arrived while the upstream WebSocket\n * was not yet open (e.g. during a reconnect). Flushed once upstream opens.\n */\nconst pendingUpstreamMessages: {\n data: WebSocket.RawData;\n isBinary: boolean;\n}[] = [];\n\n/**\n * Create a new upstream WebSocket to Chrome and bind its event handlers.\n * Used for both initial connection and reconnection.\n */\nfunction createUpstream(endpoint: string): WebSocket {\n const ws = new WebSocket(endpoint);\n\n ws.on('error', (err) => {\n // A failed reconnect must not leave the flag stuck on.\n reconnecting = false;\n shutdown(`upstream error: ${err.message}`);\n });\n\n ws.on('close', (code, reasonBuf) => {\n if (reconnecting) return;\n const reason = reasonBuf?.toString?.() || '';\n const detail = reason\n ? ` (code=${code}, reason=${reason})`\n : ` (code=${code})`;\n shutdown(`upstream closed${detail}`);\n });\n\n // Forward upstream messages to all downstream clients\n ws.on('message', (data, isBinary) => {\n resetIdleTimer();\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n }\n });\n\n ws.on('open', () => {\n reconnecting = false;\n for (const msg of pendingUpstreamMessages) {\n ws.send(msg.data, { binary: msg.isBinary });\n }\n pendingUpstreamMessages.length = 0;\n });\n\n return ws;\n}\n\nlet upstream = createUpstream(chromeEndpoint);\n\n/**\n * Reconnect the upstream WebSocket to Chrome.\n *\n * Called when all downstream clients have disconnected. This resets the CDP\n * protocol state on Chrome's side — critically, the Target.setDiscoverTargets\n * subscription — so the next client that connects gets a fresh session and\n * receives all targetCreated events.\n *\n * `reconnecting` is cleared by the new upstream's `open` (or `error`) handler,\n * not here — the new socket is still CONNECTING when this returns.\n */\nfunction reconnectUpstream() {\n reconnecting = true;\n upstream.removeAllListeners();\n upstream.close();\n upstream = createUpstream(chromeEndpoint);\n resetIdleTimer();\n}\n\n// ---------------------------------------------------------------------------\n// Downstream: local WebSocket server\n// ---------------------------------------------------------------------------\n\nconst httpServer = createServer((_req, res) => {\n res.writeHead(404);\n res.end();\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\n\nwss.on('connection', (clientWs) => {\n // Reconnect the upstream WebSocket if the previous session ended.\n // This resets Chrome's CDP protocol state (Target.setDiscoverTargets, etc.)\n // so the new client receives all targetCreated events.\n if (needsUpstreamReconnect && upstream.readyState === WebSocket.OPEN) {\n reconnectUpstream();\n needsUpstreamReconnect = false;\n }\n\n clients.add(clientWs);\n resetIdleTimer();\n\n // Forward downstream messages to upstream (buffer if upstream is reconnecting)\n clientWs.on('message', (data, isBinary) => {\n resetIdleTimer();\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n } else {\n pendingUpstreamMessages.push({ data, isBinary });\n }\n });\n\n const removeClient = () => {\n clients.delete(clientWs);\n // When all downstream clients disconnect, mark that the upstream needs\n // reconnecting to reset Chrome's CDP protocol state. The actual\n // reconnect is deferred until the next client connects so we don't\n // trigger Chrome's permission popup while nobody is using the proxy.\n if (clients.size === 0) {\n pendingUpstreamMessages.length = 0;\n needsUpstreamReconnect = true;\n }\n };\n\n clientWs.on('close', removeClient);\n clientWs.on('error', removeClient);\n});\n\n// ---------------------------------------------------------------------------\n// Start\n// ---------------------------------------------------------------------------\n\n// Start the HTTP/WebSocket server once the initial upstream connection opens.\n// This listener is added *after* createUpstream() already bound its own 'open'\n// handler (which flushes pendingUpstreamMessages), so both will fire.\nupstream.once('open', () => {\n // Check for duplicate proxy\n if (existsSync(PROXY_PID_FILE)) {\n try {\n const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (existingPid !== process.pid) {\n try {\n process.kill(existingPid, 0);\n // Another proxy is alive — exit without cleanupIfOwned() (we don't\n // own the metadata files). Announce the reason on stderr so the\n // parent process can distinguish this path from upstream failures,\n // then bail out of this open handler so we don't proceed to listen().\n exitWithStderr(\n `[cdp-proxy] duplicate proxy detected (existing pid=${existingPid})\\n`,\n 0,\n );\n return;\n } catch {\n // dead — we take over\n }\n }\n } catch {}\n }\n\n httpServer.listen(0, '127.0.0.1', () => {\n const addr = httpServer.address();\n if (!addr || typeof addr === 'string') {\n shutdown('failed to get server address');\n return;\n }\n\n const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;\n\n writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);\n writeFileSync(PROXY_PID_FILE, String(process.pid));\n writeFileSync(PROXY_UPSTREAM_FILE, chromeEndpoint);\n\n process.stdout.write(`${JSON.stringify({ endpoint: proxyEndpoint })}\\n`);\n });\n});\n"],"names":["IDLE_TIMEOUT_MS","chromeEndpoint","process","cleanupIfOwned","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","PROXY_ENDPOINT_FILE","unlinkSync","PROXY_UPSTREAM_FILE","STDERR_FLUSH_FALLBACK_MS","exitWithStderr","message","code","exited","doExit","fallback","setTimeout","shutdown","reason","e","idleTimer","resetIdleTimer","clearTimeout","clients","Set","reconnecting","needsUpstreamReconnect","pendingUpstreamMessages","createUpstream","endpoint","ws","WebSocket","err","reasonBuf","detail","data","isBinary","client","msg","upstream","reconnectUpstream","httpServer","createServer","_req","res","wss","WebSocketServer","clientWs","removeClient","existingPid","addr","proxyEndpoint","writeFileSync","String","JSON"],"mappings":"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"}
1
+ {"version":3,"file":"cdp-proxy.mjs","sources":["../../src/cdp-proxy.ts"],"sourcesContent":["/**\n * CDP WebSocket Proxy — standalone process.\n *\n * Holds a persistent WebSocket connection to Chrome's CDP endpoint and\n * exposes a local WebSocket server. Midscene CLI processes connect to the\n * proxy instead of Chrome directly, so Chrome's \"Allow remote debugging\"\n * permission popup only fires once (when the proxy connects).\n *\n * Lifecycle notes:\n * - When all downstream clients disconnect the proxy stays running but\n * marks the upstream as needing reconnection. The actual reconnect is\n * deferred to the moment the next client connects, so Chrome's CDP\n * state (notably Target.setDiscoverTargets) is reset and the new\n * client receives all targetCreated events.\n * - On startup, if another proxy is already alive the new instance\n * announces \"duplicate proxy detected\" on stderr and exits 0 without\n * touching the existing metadata files.\n *\n * Exit conditions:\n * 1. Upstream Chrome connection closes or errors.\n * 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).\n * 3. SIGTERM / SIGINT.\n * 4. Duplicate proxy detected on startup (exits 0 with stderr notice).\n *\n * Usage (spawned by agent-tools-cdp.ts):\n * node cdp-proxy.js <chrome-ws-endpoint>\n *\n * On startup, prints the proxy endpoint to stdout as a single JSON line:\n * {\"endpoint\":\"ws://127.0.0.1:<port>/devtools/browser\"}\n * and writes:\n * - PROXY_ENDPOINT_FILE — the local proxy URL above\n * - PROXY_PID_FILE — this process's pid\n * - PROXY_UPSTREAM_FILE — the Chrome endpoint the proxy is connected to,\n * so callers can detect when the requested\n * upstream has changed and replace the proxy.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n PROXY_ENDPOINT_FILE,\n PROXY_PID_FILE,\n PROXY_UPSTREAM_FILE,\n} from './cdp-proxy-constants';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\nconst chromeEndpoint = process.argv[2];\nif (!chromeEndpoint) {\n process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\\n');\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Cleanup\n// ---------------------------------------------------------------------------\n\nfunction cleanupIfOwned() {\n // Only clean up if the PID file exists and records *our* PID.\n // If the file is missing (e.g. already deleted by killProxy()) or\n // unreadable, skip cleanup — another process may have taken over.\n try {\n if (!existsSync(PROXY_PID_FILE)) return;\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (pid !== process.pid) return;\n } catch {\n return;\n }\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_UPSTREAM_FILE)) unlinkSync(PROXY_UPSTREAM_FILE);\n } catch {}\n}\n\n/**\n * Maximum time to wait for the stderr drain callback before forcing exit.\n * The callback should normally fire within microseconds, but if the pipe\n * has been closed by the parent it may never run. 500ms is generous\n * enough to be effectively unreachable in the happy path while still\n * keeping the process from hanging if something goes wrong.\n */\nconst STDERR_FLUSH_FALLBACK_MS = 500;\n\n/**\n * Exit after the stderr diagnostic has been flushed.\n *\n * When the proxy's stderr is a pipe (parent uses stdio 'pipe'), Node's\n * process.stderr.write() is asynchronous on POSIX. Calling process.exit()\n * immediately afterwards can drop the pending write, which would silently\n * lose the very diagnostic the caller is relying on. Wait for the drain\n * callback before exiting, with a short fallback timer in case the\n * callback never fires (e.g. the pipe is already closed).\n */\nfunction exitWithStderr(message: string, code = 0): void {\n let exited = false;\n const doExit = () => {\n if (exited) return;\n exited = true;\n process.exit(code);\n };\n const fallback = setTimeout(doExit, STDERR_FLUSH_FALLBACK_MS);\n fallback.unref?.();\n try {\n process.stderr.write(message, () => doExit());\n } catch {\n doExit();\n }\n}\n\nfunction shutdown(reason: string): void {\n cleanupIfOwned();\n exitWithStderr(`[cdp-proxy] shutting down: ${reason}\\n`, 0);\n}\n\nprocess.on('SIGTERM', () => shutdown('SIGTERM'));\nprocess.on('SIGINT', () => shutdown('SIGINT'));\nprocess.on('uncaughtException', (e) => shutdown(`uncaught: ${e.message}`));\n\n// ---------------------------------------------------------------------------\n// Idle timer\n// ---------------------------------------------------------------------------\n\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction resetIdleTimer() {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(\n () => shutdown('idle timeout (5min)'),\n IDLE_TIMEOUT_MS,\n );\n}\n\nresetIdleTimer();\n\n// ---------------------------------------------------------------------------\n// Upstream: connect to Chrome\n// ---------------------------------------------------------------------------\n\nconst clients = new Set<WebSocket>();\n\n/**\n * Whether we are intentionally reconnecting the upstream WebSocket.\n * When true, the old upstream's close/error events should not trigger shutdown.\n */\nlet reconnecting = false;\n\n/**\n * Whether the upstream WebSocket needs to be reconnected before the next\n * client can use it. Set to true when all downstream clients disconnect;\n * the actual reconnect is deferred until a new client connects so that\n * Chrome's permission popup only fires when someone actually needs it.\n */\nlet needsUpstreamReconnect = false;\n\n/**\n * Messages from downstream clients that arrived while the upstream WebSocket\n * was not yet open (e.g. during a reconnect). Flushed once upstream opens.\n */\nconst pendingUpstreamMessages: {\n data: WebSocket.RawData;\n isBinary: boolean;\n}[] = [];\n\n/**\n * Create a new upstream WebSocket to Chrome and bind its event handlers.\n * Used for both initial connection and reconnection.\n */\nfunction createUpstream(endpoint: string): WebSocket {\n const ws = new WebSocket(endpoint);\n\n ws.on('error', (err) => {\n // A failed reconnect must not leave the flag stuck on.\n reconnecting = false;\n shutdown(`upstream error: ${err.message}`);\n });\n\n ws.on('close', (code, reasonBuf) => {\n if (reconnecting) return;\n const reason = reasonBuf?.toString?.() || '';\n const detail = reason\n ? ` (code=${code}, reason=${reason})`\n : ` (code=${code})`;\n shutdown(`upstream closed${detail}`);\n });\n\n // Forward upstream messages to all downstream clients\n ws.on('message', (data, isBinary) => {\n resetIdleTimer();\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n }\n });\n\n ws.on('open', () => {\n reconnecting = false;\n for (const msg of pendingUpstreamMessages) {\n ws.send(msg.data, { binary: msg.isBinary });\n }\n pendingUpstreamMessages.length = 0;\n });\n\n return ws;\n}\n\nlet upstream = createUpstream(chromeEndpoint);\n\n/**\n * Reconnect the upstream WebSocket to Chrome.\n *\n * Called when all downstream clients have disconnected. This resets the CDP\n * protocol state on Chrome's side — critically, the Target.setDiscoverTargets\n * subscription — so the next client that connects gets a fresh session and\n * receives all targetCreated events.\n *\n * `reconnecting` is cleared by the new upstream's `open` (or `error`) handler,\n * not here — the new socket is still CONNECTING when this returns.\n */\nfunction reconnectUpstream() {\n reconnecting = true;\n upstream.removeAllListeners();\n upstream.close();\n upstream = createUpstream(chromeEndpoint);\n resetIdleTimer();\n}\n\n// ---------------------------------------------------------------------------\n// Downstream: local WebSocket server\n// ---------------------------------------------------------------------------\n\nconst httpServer = createServer((_req, res) => {\n res.writeHead(404);\n res.end();\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\n\nwss.on('connection', (clientWs) => {\n // Reconnect the upstream WebSocket if the previous session ended.\n // This resets Chrome's CDP protocol state (Target.setDiscoverTargets, etc.)\n // so the new client receives all targetCreated events.\n if (needsUpstreamReconnect && upstream.readyState === WebSocket.OPEN) {\n reconnectUpstream();\n needsUpstreamReconnect = false;\n }\n\n clients.add(clientWs);\n resetIdleTimer();\n\n // Forward downstream messages to upstream (buffer if upstream is reconnecting)\n clientWs.on('message', (data, isBinary) => {\n resetIdleTimer();\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n } else {\n pendingUpstreamMessages.push({ data, isBinary });\n }\n });\n\n const removeClient = () => {\n clients.delete(clientWs);\n // When all downstream clients disconnect, mark that the upstream needs\n // reconnecting to reset Chrome's CDP protocol state. The actual\n // reconnect is deferred until the next client connects so we don't\n // trigger Chrome's permission popup while nobody is using the proxy.\n if (clients.size === 0) {\n pendingUpstreamMessages.length = 0;\n needsUpstreamReconnect = true;\n }\n };\n\n clientWs.on('close', removeClient);\n clientWs.on('error', removeClient);\n});\n\n// ---------------------------------------------------------------------------\n// Start\n// ---------------------------------------------------------------------------\n\n// Start the HTTP/WebSocket server once the initial upstream connection opens.\n// This listener is added *after* createUpstream() already bound its own 'open'\n// handler (which flushes pendingUpstreamMessages), so both will fire.\nupstream.once('open', () => {\n // Check for duplicate proxy\n if (existsSync(PROXY_PID_FILE)) {\n try {\n const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (existingPid !== process.pid) {\n try {\n process.kill(existingPid, 0);\n // Another proxy is alive — exit without cleanupIfOwned() (we don't\n // own the metadata files). Announce the reason on stderr so the\n // parent process can distinguish this path from upstream failures,\n // then bail out of this open handler so we don't proceed to listen().\n exitWithStderr(\n `[cdp-proxy] duplicate proxy detected (existing pid=${existingPid})\\n`,\n 0,\n );\n return;\n } catch {\n // dead — we take over\n }\n }\n } catch {}\n }\n\n httpServer.listen(0, '127.0.0.1', () => {\n const addr = httpServer.address();\n if (!addr || typeof addr === 'string') {\n shutdown('failed to get server address');\n return;\n }\n\n const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;\n\n writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);\n writeFileSync(PROXY_PID_FILE, String(process.pid));\n writeFileSync(PROXY_UPSTREAM_FILE, chromeEndpoint);\n\n process.stdout.write(`${JSON.stringify({ endpoint: proxyEndpoint })}\\n`);\n });\n});\n"],"names":["IDLE_TIMEOUT_MS","chromeEndpoint","process","cleanupIfOwned","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","PROXY_ENDPOINT_FILE","unlinkSync","PROXY_UPSTREAM_FILE","STDERR_FLUSH_FALLBACK_MS","exitWithStderr","message","code","exited","doExit","fallback","setTimeout","shutdown","reason","e","idleTimer","resetIdleTimer","clearTimeout","clients","Set","reconnecting","needsUpstreamReconnect","pendingUpstreamMessages","createUpstream","endpoint","ws","WebSocket","err","reasonBuf","detail","data","isBinary","client","msg","upstream","reconnectUpstream","httpServer","createServer","_req","res","wss","WebSocketServer","clientWs","removeClient","existingPid","addr","proxyEndpoint","writeFileSync","String","JSON"],"mappings":";;;;AAkDA,MAAMA,kBAAkB;AAExB,MAAMC,iBAAiBC,QAAQ,IAAI,CAAC,EAAE;AACtC,IAAI,CAACD,gBAAgB;IACnBC,QAAQ,MAAM,CAAC,KAAK,CAAC;IACrBA,QAAQ,IAAI,CAAC;AACf;AAMA,SAASC;IAIP,IAAI;QACF,IAAI,CAACC,WAAWC,iBAAiB;QACjC,MAAMC,MAAMC,OAAOC,aAAaH,gBAAgB,SAAS,IAAI;QAC7D,IAAIC,QAAQJ,QAAQ,GAAG,EAAE;IAC3B,EAAE,OAAM;QACN;IACF;IACA,IAAI;QACF,IAAIE,WAAWK,sBAAsBC,WAAWD;IAClD,EAAE,OAAM,CAAC;IACT,IAAI;QACF,IAAIL,WAAWC,iBAAiBK,WAAWL;IAC7C,EAAE,OAAM,CAAC;IACT,IAAI;QACF,IAAID,WAAWO,sBAAsBD,WAAWC;IAClD,EAAE,OAAM,CAAC;AACX;AASA,MAAMC,2BAA2B;AAYjC,SAASC,eAAeC,OAAe,EAAEC,OAAO,CAAC;IAC/C,IAAIC,SAAS;IACb,MAAMC,SAAS;QACb,IAAID,QAAQ;QACZA,SAAS;QACTd,QAAQ,IAAI,CAACa;IACf;IACA,MAAMG,WAAWC,WAAWF,QAAQL;IACpCM,SAAS,KAAK;IACd,IAAI;QACFhB,QAAQ,MAAM,CAAC,KAAK,CAACY,SAAS,IAAMG;IACtC,EAAE,OAAM;QACNA;IACF;AACF;AAEA,SAASG,SAASC,MAAc;IAC9BlB;IACAU,eAAe,CAAC,2BAA2B,EAAEQ,OAAO,EAAE,CAAC,EAAE;AAC3D;AAEAnB,QAAQ,EAAE,CAAC,WAAW,IAAMkB,SAAS;AACrClB,QAAQ,EAAE,CAAC,UAAU,IAAMkB,SAAS;AACpClB,QAAQ,EAAE,CAAC,qBAAqB,CAACoB,IAAMF,SAAS,CAAC,UAAU,EAAEE,EAAE,OAAO,EAAE;AAMxE,IAAIC,YAAkD;AAEtD,SAASC;IACP,IAAID,WAAWE,aAAaF;IAC5BA,YAAYJ,WACV,IAAMC,SAAS,wBACfpB;AAEJ;AAEAwB;AAMA,MAAME,UAAU,IAAIC;AAMpB,IAAIC,eAAe;AAQnB,IAAIC,yBAAyB;AAM7B,MAAMC,0BAGA,EAAE;AAMR,SAASC,eAAeC,QAAgB;IACtC,MAAMC,KAAK,IAAIC,KAAUF;IAEzBC,GAAG,EAAE,CAAC,SAAS,CAACE;QAEdP,eAAe;QACfR,SAAS,CAAC,gBAAgB,EAAEe,IAAI,OAAO,EAAE;IAC3C;IAEAF,GAAG,EAAE,CAAC,SAAS,CAAClB,MAAMqB;QACpB,IAAIR,cAAc;QAClB,MAAMP,SAASe,WAAW,gBAAgB;QAC1C,MAAMC,SAAShB,SACX,CAAC,OAAO,EAAEN,KAAK,SAAS,EAAEM,OAAO,CAAC,CAAC,GACnC,CAAC,OAAO,EAAEN,KAAK,CAAC,CAAC;QACrBK,SAAS,CAAC,eAAe,EAAEiB,QAAQ;IACrC;IAGAJ,GAAG,EAAE,CAAC,WAAW,CAACK,MAAMC;QACtBf;QACA,KAAK,MAAMgB,UAAUd,QACnB,IAAIc,OAAO,UAAU,KAAKN,KAAAA,IAAc,EACtCM,OAAO,IAAI,CAACF,MAAM;YAAE,QAAQC;QAAS;IAG3C;IAEAN,GAAG,EAAE,CAAC,QAAQ;QACZL,eAAe;QACf,KAAK,MAAMa,OAAOX,wBAChBG,GAAG,IAAI,CAACQ,IAAI,IAAI,EAAE;YAAE,QAAQA,IAAI,QAAQ;QAAC;QAE3CX,wBAAwB,MAAM,GAAG;IACnC;IAEA,OAAOG;AACT;AAEA,IAAIS,WAAWX,eAAe9B;AAa9B,SAAS0C;IACPf,eAAe;IACfc,SAAS,kBAAkB;IAC3BA,SAAS,KAAK;IACdA,WAAWX,eAAe9B;IAC1BuB;AACF;AAMA,MAAMoB,aAAaC,aAAa,CAACC,MAAMC;IACrCA,IAAI,SAAS,CAAC;IACdA,IAAI,GAAG;AACT;AAEA,MAAMC,MAAM,IAAIC,gBAAgB;IAAE,QAAQL;AAAW;AAErDI,IAAI,EAAE,CAAC,cAAc,CAACE;IAIpB,IAAIrB,0BAA0Ba,SAAS,UAAU,KAAKR,KAAAA,IAAc,EAAE;QACpES;QACAd,yBAAyB;IAC3B;IAEAH,QAAQ,GAAG,CAACwB;IACZ1B;IAGA0B,SAAS,EAAE,CAAC,WAAW,CAACZ,MAAMC;QAC5Bf;QACA,IAAIkB,SAAS,UAAU,KAAKR,KAAAA,IAAc,EACxCQ,SAAS,IAAI,CAACJ,MAAM;YAAE,QAAQC;QAAS;aAEvCT,wBAAwB,IAAI,CAAC;YAAEQ;YAAMC;QAAS;IAElD;IAEA,MAAMY,eAAe;QACnBzB,QAAQ,MAAM,CAACwB;QAKf,IAAIxB,AAAiB,MAAjBA,QAAQ,IAAI,EAAQ;YACtBI,wBAAwB,MAAM,GAAG;YACjCD,yBAAyB;QAC3B;IACF;IAEAqB,SAAS,EAAE,CAAC,SAASC;IACrBD,SAAS,EAAE,CAAC,SAASC;AACvB;AASAT,SAAS,IAAI,CAAC,QAAQ;IAEpB,IAAItC,WAAWC,iBACb,IAAI;QACF,MAAM+C,cAAc7C,OAAOC,aAAaH,gBAAgB,SAAS,IAAI;QACrE,IAAI+C,gBAAgBlD,QAAQ,GAAG,EAC7B,IAAI;YACFA,QAAQ,IAAI,CAACkD,aAAa;YAK1BvC,eACE,CAAC,mDAAmD,EAAEuC,YAAY,GAAG,CAAC,EACtE;YAEF;QACF,EAAE,OAAM,CAER;IAEJ,EAAE,OAAM,CAAC;IAGXR,WAAW,MAAM,CAAC,GAAG,aAAa;QAChC,MAAMS,OAAOT,WAAW,OAAO;QAC/B,IAAI,CAACS,QAAQ,AAAgB,YAAhB,OAAOA,MAAmB,YACrCjC,SAAS;QAIX,MAAMkC,gBAAgB,CAAC,eAAe,EAAED,KAAK,IAAI,CAAC,iBAAiB,CAAC;QAEpEE,cAAc9C,qBAAqB6C;QACnCC,cAAclD,gBAAgBmD,OAAOtD,QAAQ,GAAG;QAChDqD,cAAc5C,qBAAqBV;QAEnCC,QAAQ,MAAM,CAAC,KAAK,CAAC,GAAGuD,KAAK,SAAS,CAAC;YAAE,UAAUH;QAAc,GAAG,EAAE,CAAC;IACzE;AACF"}
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { getDebug } from "@midscene/shared/logger";
3
3
  import { TARGET_ID_FILE } from "./cdp-proxy-constants.mjs";
4
- const debug = getDebug('mcp:cdp:target-store');
4
+ const debug = getDebug('agent-tools:cdp:target-store');
5
5
  function readSavedTargetId() {
6
6
  if (!existsSync(TARGET_ID_FILE)) return null;
7
7
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-target-store.mjs","sources":["../../src/cdp-target-store.ts"],"sourcesContent":["/**\n * Persistent store for the CDP-mode \"current tab\" targetId.\n *\n * The Midscene CLI runs each command as a fresh Node process, so anything\n * the previous command knew about which tab was being driven must survive\n * across processes. This store writes the chosen targetId to a temp file\n * after `connect`/`act`/etc. succeed, and the next command reads it back\n * to bind to the exact same tab — even when Chrome holds 14 of them.\n *\n * Owns nothing else. The CDP proxy lifecycle and its own metadata files\n * live in `cdp-proxy-manager.ts`.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { getDebug } from '@midscene/shared/logger';\nimport { TARGET_ID_FILE } from './cdp-proxy-constants';\n\nconst debug = getDebug('mcp:cdp:target-store');\n\n/**\n * Read the saved targetId, or null if no command has stored one yet.\n */\nexport function readSavedTargetId(): string | null {\n if (!existsSync(TARGET_ID_FILE)) return null;\n try {\n return readFileSync(TARGET_ID_FILE, 'utf-8').trim() || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a targetId so the next CLI command can rebind to the same tab.\n */\nexport function saveTargetId(targetId: string): void {\n try {\n writeFileSync(TARGET_ID_FILE, targetId, 'utf-8');\n debug('Saved targetId: %s', targetId);\n } catch (err) {\n debug('Failed to save targetId: %s', err);\n }\n}\n\n/**\n * Discard the saved targetId — call when disconnecting or when the\n * upstream Chrome changes (the targetId would point into the old\n * browser's tab list).\n */\nexport function cleanupTargetIdFile(): void {\n try {\n if (existsSync(TARGET_ID_FILE)) unlinkSync(TARGET_ID_FILE);\n } catch {}\n}\n"],"names":["debug","getDebug","readSavedTargetId","existsSync","TARGET_ID_FILE","readFileSync","saveTargetId","targetId","writeFileSync","err","cleanupTargetIdFile","unlinkSync"],"mappings":";;;AAiBA,MAAMA,QAAQC,SAAS;AAKhB,SAASC;IACd,IAAI,CAACC,WAAWC,iBAAiB,OAAO;IACxC,IAAI;QACF,OAAOC,aAAaD,gBAAgB,SAAS,IAAI,MAAM;IACzD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAKO,SAASE,aAAaC,QAAgB;IAC3C,IAAI;QACFC,cAAcJ,gBAAgBG,UAAU;QACxCP,MAAM,sBAAsBO;IAC9B,EAAE,OAAOE,KAAK;QACZT,MAAM,+BAA+BS;IACvC;AACF;AAOO,SAASC;IACd,IAAI;QACF,IAAIP,WAAWC,iBAAiBO,WAAWP;IAC7C,EAAE,OAAM,CAAC;AACX"}
1
+ {"version":3,"file":"cdp-target-store.mjs","sources":["../../src/cdp-target-store.ts"],"sourcesContent":["/**\n * Persistent store for the CDP-mode \"current tab\" targetId.\n *\n * The Midscene CLI runs each command as a fresh Node process, so anything\n * the previous command knew about which tab was being driven must survive\n * across processes. This store writes the chosen targetId to a temp file\n * after `connect`/`act`/etc. succeed, and the next command reads it back\n * to bind to the exact same tab — even when Chrome holds 14 of them.\n *\n * Owns nothing else. The CDP proxy lifecycle and its own metadata files\n * live in `cdp-proxy-manager.ts`.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { getDebug } from '@midscene/shared/logger';\nimport { TARGET_ID_FILE } from './cdp-proxy-constants';\n\nconst debug = getDebug('agent-tools:cdp:target-store');\n\n/**\n * Read the saved targetId, or null if no command has stored one yet.\n */\nexport function readSavedTargetId(): string | null {\n if (!existsSync(TARGET_ID_FILE)) return null;\n try {\n return readFileSync(TARGET_ID_FILE, 'utf-8').trim() || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a targetId so the next CLI command can rebind to the same tab.\n */\nexport function saveTargetId(targetId: string): void {\n try {\n writeFileSync(TARGET_ID_FILE, targetId, 'utf-8');\n debug('Saved targetId: %s', targetId);\n } catch (err) {\n debug('Failed to save targetId: %s', err);\n }\n}\n\n/**\n * Discard the saved targetId — call when disconnecting or when the\n * upstream Chrome changes (the targetId would point into the old\n * browser's tab list).\n */\nexport function cleanupTargetIdFile(): void {\n try {\n if (existsSync(TARGET_ID_FILE)) unlinkSync(TARGET_ID_FILE);\n } catch {}\n}\n"],"names":["debug","getDebug","readSavedTargetId","existsSync","TARGET_ID_FILE","readFileSync","saveTargetId","targetId","writeFileSync","err","cleanupTargetIdFile","unlinkSync"],"mappings":";;;AAiBA,MAAMA,QAAQC,SAAS;AAKhB,SAASC;IACd,IAAI,CAACC,WAAWC,iBAAiB,OAAO;IACxC,IAAI;QACF,OAAOC,aAAaD,gBAAgB,SAAS,IAAI,MAAM;IACzD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAKO,SAASE,aAAaC,QAAgB;IAC3C,IAAI;QACFC,cAAcJ,gBAAgBG,UAAU;QACxCP,MAAM,sBAAsBO;IAC9B,EAAE,OAAOE,KAAK;QACZT,MAAM,+BAA+BS;IACvC;AACF;AAOO,SAASC;IACd,IAAI;QACF,IAAIP,WAAWC,iBAAiBO,WAAWP;IAC7C,EAAE,OAAM,CAAC;AACX"}
package/dist/es/cli.mjs CHANGED
@@ -3,10 +3,10 @@ import { join } from "node:path";
3
3
  import { createReportCliCommands } from "@midscene/core";
4
4
  import { reportCLIError, runToolsCLI } from "@midscene/shared/cli";
5
5
  import dotenv from "dotenv";
6
+ import { WebMidsceneTools } from "./agent-tools.mjs";
7
+ import { WebCdpMidsceneTools } from "./agent-tools-cdp.mjs";
8
+ import { WebPuppeteerMidsceneTools } from "./agent-tools-puppeteer.mjs";
6
9
  import { parseWebCliOptions } from "./cli-options.mjs";
7
- import { WebMidsceneTools } from "./mcp-tools.mjs";
8
- import { WebCdpMidsceneTools } from "./mcp-tools-cdp.mjs";
9
- import { WebPuppeteerMidsceneTools } from "./mcp-tools-puppeteer.mjs";
10
10
  const envFile = join(process.cwd(), '.env');
11
11
  if (existsSync(envFile)) dotenv.config({
12
12
  path: envFile
@@ -18,7 +18,7 @@ Promise.resolve().then(()=>{
18
18
  return runToolsCLI(tools, 'midscene-web', {
19
19
  stripPrefix: 'web_',
20
20
  argv: parsedOptions.argv,
21
- version: "1.9.8",
21
+ version: "1.10.1-beta-20260624112700.0",
22
22
  extraCommands: createReportCliCommands()
23
23
  });
24
24
  }).catch((e)=>{
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","sources":["../../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createReportCliCommands } from '@midscene/core';\nimport { reportCLIError, runToolsCLI } from '@midscene/shared/cli';\nimport dotenv from 'dotenv';\nimport { parseWebCliOptions } from './cli-options';\nimport { WebMidsceneTools } from './mcp-tools';\nimport { WebCdpMidsceneTools } from './mcp-tools-cdp';\nimport { WebPuppeteerMidsceneTools } from './mcp-tools-puppeteer';\n\n// Load .env early so MIDSCENE_CDP_ENDPOINT is available during arg parsing\nconst envFile = join(process.cwd(), '.env');\nif (existsSync(envFile)) {\n dotenv.config({ path: envFile });\n}\n\ndeclare const __VERSION__: string;\n\nPromise.resolve()\n .then(() => {\n const parsedOptions = parseWebCliOptions(process.argv.slice(2));\n\n let tools:\n | WebMidsceneTools\n | WebPuppeteerMidsceneTools\n | WebCdpMidsceneTools;\n if (parsedOptions.mode === 'bridge') {\n tools = new WebMidsceneTools();\n } else if (parsedOptions.mode === 'cdp') {\n tools = new WebCdpMidsceneTools(parsedOptions.cdpEndpoint!);\n } else {\n tools = new WebPuppeteerMidsceneTools(parsedOptions.viewport);\n }\n\n return runToolsCLI(tools, 'midscene-web', {\n stripPrefix: 'web_',\n argv: parsedOptions.argv,\n version: __VERSION__,\n extraCommands: createReportCliCommands(),\n });\n })\n .catch((e) => {\n process.exit(reportCLIError(e));\n });\n"],"names":["envFile","join","process","existsSync","dotenv","Promise","parsedOptions","parseWebCliOptions","tools","WebMidsceneTools","WebCdpMidsceneTools","WebPuppeteerMidsceneTools","runToolsCLI","__VERSION__","createReportCliCommands","e","reportCLIError"],"mappings":";;;;;;;;;AAWA,MAAMA,UAAUC,KAAKC,QAAQ,GAAG,IAAI;AACpC,IAAIC,WAAWH,UACbI,OAAO,MAAM,CAAC;IAAE,MAAMJ;AAAQ;AAKhCK,QAAQ,OAAO,GACZ,IAAI,CAAC;IACJ,MAAMC,gBAAgBC,mBAAmBL,QAAQ,IAAI,CAAC,KAAK,CAAC;IAE5D,IAAIM;IAKFA,QADEF,AAAuB,aAAvBA,cAAc,IAAI,GACZ,IAAIG,qBACHH,AAAuB,UAAvBA,cAAc,IAAI,GACnB,IAAII,oBAAoBJ,cAAc,WAAW,IAEjD,IAAIK,0BAA0BL,cAAc,QAAQ;IAG9D,OAAOM,YAAYJ,OAAO,gBAAgB;QACxC,aAAa;QACb,MAAMF,cAAc,IAAI;QACxB,SAASO;QACT,eAAeC;IACjB;AACF,GACC,KAAK,CAAC,CAACC;IACNb,QAAQ,IAAI,CAACc,eAAeD;AAC9B"}
1
+ {"version":3,"file":"cli.mjs","sources":["../../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createReportCliCommands } from '@midscene/core';\nimport { reportCLIError, runToolsCLI } from '@midscene/shared/cli';\nimport dotenv from 'dotenv';\nimport { WebMidsceneTools } from './agent-tools';\nimport { WebCdpMidsceneTools } from './agent-tools-cdp';\nimport { WebPuppeteerMidsceneTools } from './agent-tools-puppeteer';\nimport { parseWebCliOptions } from './cli-options';\n\n// Load .env early so MIDSCENE_CDP_ENDPOINT is available during arg parsing\nconst envFile = join(process.cwd(), '.env');\nif (existsSync(envFile)) {\n dotenv.config({ path: envFile });\n}\n\ndeclare const __VERSION__: string;\n\nPromise.resolve()\n .then(() => {\n const parsedOptions = parseWebCliOptions(process.argv.slice(2));\n\n let tools:\n | WebMidsceneTools\n | WebPuppeteerMidsceneTools\n | WebCdpMidsceneTools;\n if (parsedOptions.mode === 'bridge') {\n tools = new WebMidsceneTools();\n } else if (parsedOptions.mode === 'cdp') {\n tools = new WebCdpMidsceneTools(parsedOptions.cdpEndpoint!);\n } else {\n tools = new WebPuppeteerMidsceneTools(parsedOptions.viewport);\n }\n\n return runToolsCLI(tools, 'midscene-web', {\n stripPrefix: 'web_',\n argv: parsedOptions.argv,\n version: __VERSION__,\n extraCommands: createReportCliCommands(),\n });\n })\n .catch((e) => {\n process.exit(reportCLIError(e));\n });\n"],"names":["envFile","join","process","existsSync","dotenv","Promise","parsedOptions","parseWebCliOptions","tools","WebMidsceneTools","WebCdpMidsceneTools","WebPuppeteerMidsceneTools","runToolsCLI","__VERSION__","createReportCliCommands","e","reportCLIError"],"mappings":";;;;;;;;;AAWA,MAAMA,UAAUC,KAAKC,QAAQ,GAAG,IAAI;AACpC,IAAIC,WAAWH,UACbI,OAAO,MAAM,CAAC;IAAE,MAAMJ;AAAQ;AAKhCK,QAAQ,OAAO,GACZ,IAAI,CAAC;IACJ,MAAMC,gBAAgBC,mBAAmBL,QAAQ,IAAI,CAAC,KAAK,CAAC;IAE5D,IAAIM;IAKFA,QADEF,AAAuB,aAAvBA,cAAc,IAAI,GACZ,IAAIG,qBACHH,AAAuB,UAAvBA,cAAc,IAAI,GACnB,IAAII,oBAAoBJ,cAAc,WAAW,IAEjD,IAAIK,0BAA0BL,cAAc,QAAQ;IAG9D,OAAOM,YAAYJ,OAAO,gBAAgB;QACxC,aAAa;QACb,MAAMF,cAAc,IAAI;QACxB,SAASO;QACT,eAAeC;IACjB;AACF,GACC,KAAK,CAAC,CAACC;IACNb,QAAQ,IAAI,CAACc,eAAeD;AAC9B"}
package/dist/es/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { PlaywrightAgent, PlaywrightAiFixture } from "./playwright/index.mjs";
2
2
  import { Agent } from "@midscene/core/agent";
3
3
  import { PuppeteerAgent } from "./puppeteer/index.mjs";
4
4
  import { StaticPage, StaticPageAgent } from "./static/index.mjs";
5
- import { WebMidsceneTools } from "./mcp-tools.mjs";
5
+ import { WebMidsceneTools } from "./agent-tools.mjs";
6
6
  import { webPlaygroundPlatform } from "./platform.mjs";
7
- import { WebCdpMidsceneTools } from "./mcp-tools-cdp.mjs";
7
+ import { WebCdpMidsceneTools } from "./agent-tools-cdp.mjs";
8
8
  export { Agent as PageAgent, PlaywrightAgent, PlaywrightAiFixture, PuppeteerAgent, StaticPage, StaticPageAgent, WebCdpMidsceneTools, WebMidsceneTools, webPlaygroundPlatform };
@@ -24,11 +24,13 @@ class PlaywrightAgent extends Agent {
24
24
  if (!page) throw new Error('[midscene] PlaywrightAgent requires a valid Playwright page instance. Please make sure to pass a valid page object.');
25
25
  const webPage = new WebPage(page, opts);
26
26
  super(webPage, opts);
27
- const { forceSameTabNavigation = true, forceChromeSelectRendering } = opts ?? {};
27
+ const { forceSameTabNavigation = true, forceChromeSelectRendering = true } = opts ?? {};
28
28
  if (forceSameTabNavigation) forceClosePopup(page, debug);
29
29
  if (forceChromeSelectRendering) {
30
- const playwrightVersion = getPlaywrightVersion();
31
- if (playwrightVersion && !semver.gte(playwrightVersion, '1.52.0')) console.warn(`[midscene:error] forceChromeSelectRendering requires Playwright >= 1.52.0, but current version is ${playwrightVersion}. This feature may not work correctly.`);
30
+ if (opts?.forceChromeSelectRendering === true) {
31
+ const playwrightVersion = getPlaywrightVersion();
32
+ if (playwrightVersion && !semver.gte(playwrightVersion, '1.52.0')) console.warn(`[midscene:error] forceChromeSelectRendering requires Playwright >= 1.52.0, but current version is ${playwrightVersion}. This feature may not work correctly.`);
33
+ }
32
34
  base_page_mjs_forceChromeSelectRendering(page);
33
35
  }
34
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"playwright/index.mjs","sources":["../../../src/playwright/index.ts"],"sourcesContent":["import { Agent as PageAgent } from '@midscene/core/agent';\nimport type { Page as PlaywrightPage } from 'playwright';\nimport { WebPage as PlaywrightWebPage } from './page';\n\nexport type {\n PlayWrightAiFixtureType,\n PlaywrightAiFixtureOptions,\n} from './ai-fixture';\nexport { PlaywrightAiFixture } from './ai-fixture';\nexport { overrideAIConfig } from '@midscene/shared/env';\nexport { WebPage as PlaywrightWebPage } from './page';\nexport type { WebPageAgentOpt } from '@/web-element';\nimport type { WebPageAgentOpt } from '@/web-element';\nimport { getDebug } from '@midscene/shared/logger';\nimport semver from 'semver';\nimport {\n BROWSER_NAVIGATION_ERROR_PATTERN,\n forceChromeSelectRendering as applyChromeSelectRendering,\n forceClosePopup,\n} from '../puppeteer/base-page';\nimport { getWebpackRequire } from '../utils';\n\nconst debug = getDebug('playwright:agent');\n\n/**\n * Get Playwright version from package.json\n */\nfunction getPlaywrightVersion(): string | null {\n try {\n const playwrightPkg = getWebpackRequire()('playwright/package.json');\n return playwrightPkg.version || null;\n } catch (error) {\n console.error('[midscene:error] Failed to get Playwright version', error);\n return null;\n }\n}\n\nexport class PlaywrightAgent extends PageAgent<PlaywrightWebPage> {\n protected isRetryableContextError(error: unknown): boolean {\n return (\n error instanceof Error &&\n BROWSER_NAVIGATION_ERROR_PATTERN.test(error.message)\n );\n }\n\n constructor(page: PlaywrightPage, opts?: WebPageAgentOpt) {\n if (!page) {\n throw new Error(\n '[midscene] PlaywrightAgent requires a valid Playwright page instance. Please make sure to pass a valid page object.',\n );\n }\n const webPage = new PlaywrightWebPage(page, opts);\n super(webPage, opts);\n\n const { forceSameTabNavigation = true, forceChromeSelectRendering } =\n opts ?? {};\n\n if (forceSameTabNavigation) {\n forceClosePopup(page, debug);\n }\n\n if (forceChromeSelectRendering) {\n // Check Playwright version requirement (>= 1.52)\n const playwrightVersion = getPlaywrightVersion();\n if (playwrightVersion && !semver.gte(playwrightVersion, '1.52.0')) {\n console.warn(\n `[midscene:error] forceChromeSelectRendering requires Playwright >= 1.52.0, but current version is ${playwrightVersion}. This feature may not work correctly.`,\n );\n }\n applyChromeSelectRendering(page);\n }\n }\n}\n"],"names":["debug","getDebug","getPlaywrightVersion","playwrightPkg","getWebpackRequire","error","console","PlaywrightAgent","PageAgent","Error","BROWSER_NAVIGATION_ERROR_PATTERN","page","opts","webPage","PlaywrightWebPage","forceSameTabNavigation","forceChromeSelectRendering","forceClosePopup","playwrightVersion","semver","applyChromeSelectRendering"],"mappings":";;;;;;;;AAsBA,MAAMA,QAAQC,SAAS;AAKvB,SAASC;IACP,IAAI;QACF,MAAMC,gBAAgBC,oBAAoB;QAC1C,OAAOD,cAAc,OAAO,IAAI;IAClC,EAAE,OAAOE,OAAO;QACdC,QAAQ,KAAK,CAAC,qDAAqDD;QACnE,OAAO;IACT;AACF;AAEO,MAAME,wBAAwBC;IACzB,wBAAwBH,KAAc,EAAW;QACzD,OACEA,iBAAiBI,SACjBC,iCAAiC,IAAI,CAACL,MAAM,OAAO;IAEvD;IAEA,YAAYM,IAAoB,EAAEC,IAAsB,CAAE;QACxD,IAAI,CAACD,MACH,MAAM,IAAIF,MACR;QAGJ,MAAMI,UAAU,IAAIC,QAAkBH,MAAMC;QAC5C,KAAK,CAACC,SAASD;QAEf,MAAM,EAAEG,yBAAyB,IAAI,EAAEC,0BAA0B,EAAE,GACjEJ,QAAQ,CAAC;QAEX,IAAIG,wBACFE,gBAAgBN,MAAMX;QAGxB,IAAIgB,4BAA4B;YAE9B,MAAME,oBAAoBhB;YAC1B,IAAIgB,qBAAqB,CAACC,OAAO,GAAG,CAACD,mBAAmB,WACtDZ,QAAQ,IAAI,CACV,CAAC,kGAAkG,EAAEY,kBAAkB,sCAAsC,CAAC;YAGlKE,yCAA2BT;QAC7B;IACF;AACF"}
1
+ {"version":3,"file":"playwright/index.mjs","sources":["../../../src/playwright/index.ts"],"sourcesContent":["import { Agent as PageAgent } from '@midscene/core/agent';\nimport type { Page as PlaywrightPage } from 'playwright';\nimport { WebPage as PlaywrightWebPage } from './page';\n\nexport type {\n PlayWrightAiFixtureType,\n PlaywrightAiFixtureOptions,\n} from './ai-fixture';\nexport { PlaywrightAiFixture } from './ai-fixture';\nexport { overrideAIConfig } from '@midscene/shared/env';\nexport { WebPage as PlaywrightWebPage } from './page';\nexport type { WebPageAgentOpt } from '@/web-element';\nimport type { WebPageAgentOpt } from '@/web-element';\nimport { getDebug } from '@midscene/shared/logger';\nimport semver from 'semver';\nimport {\n BROWSER_NAVIGATION_ERROR_PATTERN,\n forceChromeSelectRendering as applyChromeSelectRendering,\n forceClosePopup,\n} from '../puppeteer/base-page';\nimport { getWebpackRequire } from '../utils';\n\nconst debug = getDebug('playwright:agent');\n\n/**\n * Get Playwright version from package.json\n */\nfunction getPlaywrightVersion(): string | null {\n try {\n const playwrightPkg = getWebpackRequire()('playwright/package.json');\n return playwrightPkg.version || null;\n } catch (error) {\n console.error('[midscene:error] Failed to get Playwright version', error);\n return null;\n }\n}\n\nexport class PlaywrightAgent extends PageAgent<PlaywrightWebPage> {\n protected isRetryableContextError(error: unknown): boolean {\n return (\n error instanceof Error &&\n BROWSER_NAVIGATION_ERROR_PATTERN.test(error.message)\n );\n }\n\n constructor(page: PlaywrightPage, opts?: WebPageAgentOpt) {\n if (!page) {\n throw new Error(\n '[midscene] PlaywrightAgent requires a valid Playwright page instance. Please make sure to pass a valid page object.',\n );\n }\n const webPage = new PlaywrightWebPage(page, opts);\n super(webPage, opts);\n\n const { forceSameTabNavigation = true, forceChromeSelectRendering = true } =\n opts ?? {};\n\n if (forceSameTabNavigation) {\n forceClosePopup(page, debug);\n }\n\n if (forceChromeSelectRendering) {\n // Only warn about version requirements when the user explicitly opted in;\n // it is on by default, so we should not nag users on older Playwright.\n if (opts?.forceChromeSelectRendering === true) {\n const playwrightVersion = getPlaywrightVersion();\n if (playwrightVersion && !semver.gte(playwrightVersion, '1.52.0')) {\n console.warn(\n `[midscene:error] forceChromeSelectRendering requires Playwright >= 1.52.0, but current version is ${playwrightVersion}. This feature may not work correctly.`,\n );\n }\n }\n applyChromeSelectRendering(page);\n }\n }\n}\n"],"names":["debug","getDebug","getPlaywrightVersion","playwrightPkg","getWebpackRequire","error","console","PlaywrightAgent","PageAgent","Error","BROWSER_NAVIGATION_ERROR_PATTERN","page","opts","webPage","PlaywrightWebPage","forceSameTabNavigation","forceChromeSelectRendering","forceClosePopup","playwrightVersion","semver","applyChromeSelectRendering"],"mappings":";;;;;;;;AAsBA,MAAMA,QAAQC,SAAS;AAKvB,SAASC;IACP,IAAI;QACF,MAAMC,gBAAgBC,oBAAoB;QAC1C,OAAOD,cAAc,OAAO,IAAI;IAClC,EAAE,OAAOE,OAAO;QACdC,QAAQ,KAAK,CAAC,qDAAqDD;QACnE,OAAO;IACT;AACF;AAEO,MAAME,wBAAwBC;IACzB,wBAAwBH,KAAc,EAAW;QACzD,OACEA,iBAAiBI,SACjBC,iCAAiC,IAAI,CAACL,MAAM,OAAO;IAEvD;IAEA,YAAYM,IAAoB,EAAEC,IAAsB,CAAE;QACxD,IAAI,CAACD,MACH,MAAM,IAAIF,MACR;QAGJ,MAAMI,UAAU,IAAIC,QAAkBH,MAAMC;QAC5C,KAAK,CAACC,SAASD;QAEf,MAAM,EAAEG,yBAAyB,IAAI,EAAEC,6BAA6B,IAAI,EAAE,GACxEJ,QAAQ,CAAC;QAEX,IAAIG,wBACFE,gBAAgBN,MAAMX;QAGxB,IAAIgB,4BAA4B;YAG9B,IAAIJ,MAAM,+BAA+B,MAAM;gBAC7C,MAAMM,oBAAoBhB;gBAC1B,IAAIgB,qBAAqB,CAACC,OAAO,GAAG,CAACD,mBAAmB,WACtDZ,QAAQ,IAAI,CACV,CAAC,kGAAkG,EAAEY,kBAAkB,sCAAsC,CAAC;YAGpK;YACAE,yCAA2BT;QAC7B;IACF;AACF"}
@@ -179,17 +179,15 @@ class Page {
179
179
  const errorMsg = error instanceof Error ? error.message : String(error);
180
180
  console.warn(`[Midscene] Playwright screenshot failed: ${errorMsg}. Falling back to CDP screenshot.`);
181
181
  debugPage('playwright screenshot failed, trying CDP fallback: %s', error);
182
- base64 = await this.screenshotBase64ByPlaywrightCdp(page, imgType, quality);
182
+ base64 = await this.screenshotBase64ByPlaywrightCdp(imgType, quality);
183
183
  }
184
184
  } else throw new Error('Unsupported page type for screenshot');
185
185
  const endTime = Date.now();
186
186
  debugPage(`screenshotBase64 end, cost: ${endTime - startTime}ms`);
187
187
  return base64;
188
188
  }
189
- async screenshotBase64ByPlaywrightCdp(page, imgType, quality) {
190
- const browserName = page.context().browser()?.browserType().name();
191
- if (browserName && 'chromium' !== browserName) throw new Error(`CDP screenshot fallback requires Chromium-based browser, but current browser is "${browserName}".`);
192
- const client = await page.context().newCDPSession(page);
189
+ async screenshotBase64ByPlaywrightCdp(imgType, quality) {
190
+ const client = await this.createPageCdpSession('CDP screenshot fallback');
193
191
  try {
194
192
  const result = await new Promise((resolve, reject)=>{
195
193
  const timeoutId = setTimeout(()=>{
@@ -215,14 +213,18 @@ class Page {
215
213
  });
216
214
  }
217
215
  }
218
- async createScreencastCdpSession() {
216
+ async createPageCdpSession(featureName) {
219
217
  if ('puppeteer' === this.interfaceType) {
220
218
  const page = this.underlyingPage;
221
- return await page.target().createCDPSession();
219
+ const pageWithCdp = page;
220
+ if ('function' == typeof pageWithCdp.createCDPSession) return await pageWithCdp.createCDPSession();
221
+ const target = page.target?.();
222
+ if ('function' == typeof target?.createCDPSession) return await target.createCDPSession();
223
+ throw new Error(`${featureName} requires a browser page with CDP session support.`);
222
224
  }
223
225
  const page = this.underlyingPage;
224
226
  const browserName = page.context().browser()?.browserType().name();
225
- if (browserName && 'chromium' !== browserName) throw new Error(`CDP screencast requires Chromium-based browser, but current browser is "${browserName}".`);
227
+ if (browserName && 'chromium' !== browserName) throw new Error(`${featureName} requires Chromium-based browser, but current browser is "${browserName}".`);
226
228
  return await page.context().newCDPSession(page);
227
229
  }
228
230
  async waitForDomQuiet(opts) {
@@ -307,7 +309,7 @@ class Page {
307
309
  async startMjpegStream(options) {
308
310
  const { signal, onFrame, onError } = options;
309
311
  if ('function' == typeof this.underlyingPage.bringToFront) await this.underlyingPage.bringToFront();
310
- const client = await this.createScreencastCdpSession();
312
+ const client = await this.createPageCdpSession('CDP screencast');
311
313
  let stopped = false;
312
314
  const streamToken = Symbol('mjpeg-stream');
313
315
  const reportStreamError = (error)=>{
@@ -490,6 +492,22 @@ class Page {
490
492
  }
491
493
  };
492
494
  }
495
+ async selectAllByCdp() {
496
+ const client = await this.createPageCdpSession('clearInput');
497
+ try {
498
+ await client.send('Input.dispatchKeyEvent', {
499
+ type: 'rawKeyDown',
500
+ commands: [
501
+ 'selectAll'
502
+ ]
503
+ });
504
+ await client.send('Input.dispatchKeyEvent', {
505
+ type: 'keyUp'
506
+ });
507
+ } finally{
508
+ await client.detach().catch(()=>void 0);
509
+ }
510
+ }
493
511
  async clearInput(element) {
494
512
  const backspace = async ()=>{
495
513
  await sleep(100);
@@ -499,28 +517,17 @@ class Page {
499
517
  }
500
518
  ]);
501
519
  };
502
- const isMac = 'darwin' === process.platform;
503
520
  debugPage('clearInput begin');
504
- if (isMac) {
505
- if ('puppeteer' === this.interfaceType) {
506
- element && await this.mouse.click(element.center[0], element.center[1], {
507
- count: 3
508
- });
509
- await backspace();
510
- }
511
- element && await this.mouse.click(element.center[0], element.center[1]);
512
- await this.underlyingPage.keyboard.down('Meta');
513
- await this.underlyingPage.keyboard.press('a');
514
- await this.underlyingPage.keyboard.up('Meta');
515
- await backspace();
516
- } else {
517
- element && await this.mouse.click(element.center[0], element.center[1]);
518
- await this.underlyingPage.keyboard.down('Control');
519
- await this.underlyingPage.keyboard.press('a');
520
- await this.underlyingPage.keyboard.up('Control');
521
+ element && await this.mouse.click(element.center[0], element.center[1]);
522
+ try {
523
+ await this.selectAllByCdp();
521
524
  await backspace();
525
+ } catch (error) {
526
+ debugPage('clearInput cdp selectAll failed', error);
527
+ throw error;
528
+ } finally{
529
+ debugPage('clearInput end');
522
530
  }
523
- debugPage('clearInput end');
524
531
  }
525
532
  async moveToPointBeforeScroll(point) {
526
533
  if (point) await this.mouse.move(point.left, point.top);
@@ -598,7 +605,7 @@ class Page {
598
605
  async stopLoading() {
599
606
  debugPage('stop loading');
600
607
  if ('puppeteer' === this.interfaceType) {
601
- const client = await this.underlyingPage.target().createCDPSession();
608
+ const client = await this.createPageCdpSession('stopLoading');
602
609
  try {
603
610
  await client.send('Page.stopLoading');
604
611
  } finally{
@@ -704,17 +711,7 @@ class Page {
704
711
  const delay = duration / steps;
705
712
  const halfStart = startDistance / 2;
706
713
  const halfEnd = endDistance / 2;
707
- let client;
708
- if ('puppeteer' === this.interfaceType) {
709
- const page = this.underlyingPage;
710
- client = await page.target().createCDPSession();
711
- } else {
712
- if ('playwright' !== this.interfaceType) return;
713
- const page = this.underlyingPage;
714
- const browserName = page.context().browser()?.browserType().name();
715
- if (browserName && 'chromium' !== browserName) throw new Error(`Pinch gesture requires Chromium-based browser, but current browser is "${browserName}". CDP touch events are not supported in Firefox/WebKit.`);
716
- client = await page.context().newCDPSession(page);
717
- }
714
+ const client = await this.createPageCdpSession('Pinch gesture');
718
715
  try {
719
716
  await client.send('Input.dispatchTouchEvent', {
720
717
  type: 'touchStart',
@@ -758,9 +755,9 @@ class Page {
758
755
  await client.detach();
759
756
  }
760
757
  }
761
- async ensurePuppeteerFileChooserSession(page) {
758
+ async ensurePuppeteerFileChooserSession() {
762
759
  if (this.puppeteerFileChooserSession) return this.puppeteerFileChooserSession;
763
- const session = await page.target().createCDPSession();
760
+ const session = await this.createPageCdpSession('Puppeteer file chooser');
764
761
  await session.send('Page.enable');
765
762
  await session.send('DOM.enable');
766
763
  await session.send('Page.setInterceptFileChooserDialog', {
@@ -771,8 +768,7 @@ class Page {
771
768
  }
772
769
  async registerFileChooserListener(handler) {
773
770
  if ('puppeteer' !== this.interfaceType) throw new Error('registerFileChooserListener is only supported in Puppeteer');
774
- const page = this.underlyingPage;
775
- const session = await this.ensurePuppeteerFileChooserSession(page);
771
+ const session = await this.ensurePuppeteerFileChooserSession();
776
772
  if (this.puppeteerFileChooserHandler) session.off('Page.fileChooserOpened', this.puppeteerFileChooserHandler);
777
773
  let capturedError;
778
774
  this.puppeteerFileChooserHandler = async (event)=>{
@@ -858,7 +854,10 @@ function forceClosePopup(page, debugProfile) {
858
854
  }
859
855
  });
860
856
  }
857
+ const forceSelectRenderingPages = new WeakSet();
861
858
  function forceChromeSelectRendering(page) {
859
+ if (forceSelectRenderingPages.has(page)) return;
860
+ forceSelectRenderingPages.add(page);
862
861
  const styleContent = `
863
862
  /* Add by Midscene because of forceChromeSelectRendering is enabled*/
864
863
  select {