@poolzin/pool-bot 2026.2.0 → 2026.2.2
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/CHANGELOG.md +118 -0
- package/README-header.png +0 -0
- package/dist/agents/bash-tools.exec.js +76 -25
- package/dist/agents/cli-runner/helpers.js +9 -11
- package/dist/agents/context.js +1 -1
- package/dist/agents/identity.js +47 -7
- package/dist/agents/memory-search.js +25 -8
- package/dist/agents/model-catalog.js +1 -1
- package/dist/agents/model-selection.js +21 -0
- package/dist/agents/pi-embedded-block-chunker.js +117 -42
- package/dist/agents/pi-embedded-helpers/errors.js +183 -78
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +8 -10
- package/dist/agents/pi-embedded-runner/model.js +62 -3
- package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
- package/dist/agents/pi-embedded-runner/run.js +199 -46
- package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
- package/dist/agents/pi-embedded-subscribe.js +118 -29
- package/dist/agents/pi-tools.js +10 -5
- package/dist/agents/poolbot-tools.js +15 -10
- package/dist/agents/sandbox-paths.js +31 -0
- package/dist/agents/session-tool-result-guard.js +94 -15
- package/dist/agents/shell-utils.js +51 -0
- package/dist/agents/skills/bundled-context.js +23 -0
- package/dist/agents/skills/bundled-dir.js +41 -7
- package/dist/agents/skills-install.js +60 -23
- package/dist/agents/subagent-announce.js +79 -34
- package/dist/agents/tool-policy.conformance.js +14 -0
- package/dist/agents/tool-policy.js +24 -0
- package/dist/agents/tools/cron-tool.js +166 -19
- package/dist/agents/tools/discord-actions-presence.js +78 -0
- package/dist/agents/tools/image-tool.js +1 -1
- package/dist/agents/tools/message-tool.js +56 -2
- package/dist/agents/tools/sessions-history-tool.js +69 -1
- package/dist/agents/tools/web-search.js +211 -42
- package/dist/agents/usage.js +23 -1
- package/dist/agents/workspace-run.js +67 -0
- package/dist/agents/workspace-templates.js +44 -0
- package/dist/auto-reply/command-auth.js +121 -6
- package/dist/auto-reply/envelope.js +74 -82
- package/dist/auto-reply/reply/commands-compact.js +1 -0
- package/dist/auto-reply/reply/commands-context-report.js +1 -0
- package/dist/auto-reply/reply/commands-context.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +107 -60
- package/dist/auto-reply/reply/commands-ptt.js +171 -0
- package/dist/auto-reply/reply/get-reply-run.js +2 -1
- package/dist/auto-reply/reply/inbound-context.js +5 -1
- package/dist/auto-reply/reply/mentions.js +1 -1
- package/dist/auto-reply/reply/model-selection.js +3 -3
- package/dist/auto-reply/thinking.js +88 -43
- package/dist/browser/bridge-server.js +13 -0
- package/dist/browser/cdp.helpers.js +38 -24
- package/dist/browser/client-fetch.js +50 -7
- package/dist/browser/config.js +1 -10
- package/dist/browser/extension-relay.js +101 -40
- package/dist/browser/pw-ai.js +1 -1
- package/dist/browser/pw-session.js +143 -8
- package/dist/browser/pw-tools-core.interactions.js +125 -27
- package/dist/browser/pw-tools-core.responses.js +1 -1
- package/dist/browser/pw-tools-core.state.js +1 -1
- package/dist/browser/routes/agent.act.js +86 -41
- package/dist/browser/routes/dispatcher.js +4 -4
- package/dist/browser/screenshot.js +1 -1
- package/dist/browser/server.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/index.html +28 -28
- package/dist/channels/reply-prefix.js +8 -1
- package/dist/cli/cron-cli/register.cron-add.js +61 -40
- package/dist/cli/cron-cli/register.cron-edit.js +60 -34
- package/dist/cli/cron-cli/shared.js +56 -41
- package/dist/cli/dns-cli.js +26 -14
- package/dist/cli/gateway-cli/register.js +37 -19
- package/dist/cli/memory-cli.js +5 -5
- package/dist/cli/parse-bytes.js +37 -0
- package/dist/cli/update-cli.js +173 -52
- package/dist/commands/agent.js +1 -0
- package/dist/commands/auth-choice.apply.oauth.js +1 -1
- package/dist/commands/doctor-config-flow.js +61 -5
- package/dist/commands/doctor-state-migrations.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/model-allowlist.js +29 -0
- package/dist/commands/model-picker.js +2 -1
- package/dist/commands/models/list.registry.js +1 -1
- package/dist/commands/models/list.status-command.js +43 -23
- package/dist/commands/models/shared.js +15 -0
- package/dist/commands/onboard-custom.js +384 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
- package/dist/commands/onboard-skills.js +63 -38
- package/dist/commands/openai-model-default.js +41 -0
- package/dist/compat/legacy-names.js +2 -0
- package/dist/config/defaults.js +3 -2
- package/dist/config/paths.js +136 -35
- package/dist/config/plugin-auto-enable.js +21 -5
- package/dist/config/redact-snapshot.js +153 -0
- package/dist/config/schema.field-metadata.js +590 -0
- package/dist/config/schema.js +2 -2
- package/dist/config/sessions/store.js +291 -23
- package/dist/config/zod-schema.agent-defaults.js +3 -0
- package/dist/config/zod-schema.agent-runtime.js +13 -2
- package/dist/config/zod-schema.providers-core.js +142 -0
- package/dist/config/zod-schema.session.js +3 -0
- package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
- package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
- package/dist/control-ui/index.html +4 -4
- package/dist/cron/delivery.js +57 -0
- package/dist/cron/isolated-agent/delivery-target.js +18 -3
- package/dist/cron/isolated-agent/helpers.js +22 -5
- package/dist/cron/isolated-agent/run.js +172 -63
- package/dist/cron/isolated-agent/session.js +2 -0
- package/dist/cron/normalize.js +356 -28
- package/dist/cron/parse.js +10 -5
- package/dist/cron/run-log.js +35 -10
- package/dist/cron/schedule.js +41 -6
- package/dist/cron/service/jobs.js +208 -35
- package/dist/cron/service/ops.js +72 -16
- package/dist/cron/service/state.js +2 -0
- package/dist/cron/service/store.js +386 -14
- package/dist/cron/service/timer.js +390 -147
- package/dist/cron/session-reaper.js +86 -0
- package/dist/cron/store.js +23 -8
- package/dist/cron/validate-timestamp.js +43 -0
- package/dist/discord/monitor/agent-components.js +438 -0
- package/dist/discord/monitor/allow-list.js +28 -5
- package/dist/discord/monitor/gateway-registry.js +29 -0
- package/dist/discord/monitor/native-command.js +44 -23
- package/dist/discord/monitor/sender-identity.js +45 -0
- package/dist/discord/pluralkit.js +27 -0
- package/dist/discord/send.outbound.js +92 -5
- package/dist/discord/send.shared.js +60 -23
- package/dist/discord/targets.js +84 -1
- package/dist/entry.js +15 -9
- package/dist/extensionAPI.js +8 -0
- package/dist/gateway/control-ui.js +8 -1
- package/dist/gateway/hooks-mapping.js +3 -0
- package/dist/gateway/hooks.js +65 -0
- package/dist/gateway/net.js +96 -31
- package/dist/gateway/node-command-policy.js +50 -15
- package/dist/gateway/origin-check.js +56 -0
- package/dist/gateway/protocol/client-info.js +9 -0
- package/dist/gateway/protocol/index.js +9 -2
- package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
- package/dist/gateway/protocol/schema/cron.js +22 -10
- package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
- package/dist/gateway/protocol/schema/sessions.js +12 -0
- package/dist/gateway/server/hooks.js +1 -1
- package/dist/gateway/server-broadcast.js +26 -9
- package/dist/gateway/server-chat.js +112 -23
- package/dist/gateway/server-discovery-runtime.js +10 -2
- package/dist/gateway/server-http.js +109 -11
- package/dist/gateway/server-methods/agent-timestamp.js +60 -0
- package/dist/gateway/server-methods/agents.js +321 -2
- package/dist/gateway/server-methods/usage.js +559 -16
- package/dist/gateway/server-runtime-state.js +22 -8
- package/dist/gateway/server-startup-memory.js +16 -0
- package/dist/gateway/server.impl.js +5 -1
- package/dist/gateway/session-utils.fs.js +23 -25
- package/dist/gateway/session-utils.js +20 -10
- package/dist/gateway/sessions-patch.js +7 -22
- package/dist/gateway/test-helpers.mocks.js +11 -7
- package/dist/gateway/test-helpers.server.js +35 -2
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/deliver.js +4 -1
- package/dist/imessage/monitor/monitor-provider.js +51 -1
- package/dist/infra/bonjour-discovery.js +131 -70
- package/dist/infra/control-ui-assets.js +134 -12
- package/dist/infra/errors.js +12 -0
- package/dist/infra/exec-approvals.js +266 -57
- package/dist/infra/format-time/format-datetime.js +79 -0
- package/dist/infra/format-time/format-duration.js +81 -0
- package/dist/infra/format-time/format-relative.js +80 -0
- package/dist/infra/heartbeat-runner.js +140 -49
- package/dist/infra/home-dir.js +54 -0
- package/dist/infra/net/fetch-guard.js +122 -0
- package/dist/infra/net/ssrf.js +65 -29
- package/dist/infra/outbound/abort.js +14 -0
- package/dist/infra/outbound/message-action-runner.js +77 -13
- package/dist/infra/outbound/outbound-session.js +143 -37
- package/dist/infra/poolbot-root.js +43 -1
- package/dist/infra/session-cost-usage.js +631 -41
- package/dist/infra/state-migrations.js +317 -47
- package/dist/infra/update-global.js +35 -0
- package/dist/infra/update-runner.js +149 -43
- package/dist/infra/warning-filter.js +65 -0
- package/dist/infra/widearea-dns.js +30 -9
- package/dist/logging/redact-identifier.js +12 -0
- package/dist/media/fetch.js +81 -58
- package/dist/media/store.js +2 -0
- package/dist/media-understanding/apply.js +403 -3
- package/dist/media-understanding/attachments.js +38 -27
- package/dist/media-understanding/defaults.js +16 -0
- package/dist/media-understanding/providers/deepgram/audio.js +22 -14
- package/dist/media-understanding/providers/google/audio.js +24 -17
- package/dist/media-understanding/providers/google/video.js +24 -17
- package/dist/media-understanding/providers/image.js +3 -3
- package/dist/media-understanding/providers/index.js +4 -1
- package/dist/media-understanding/providers/openai/audio.js +22 -14
- package/dist/media-understanding/providers/shared.js +16 -11
- package/dist/media-understanding/providers/zai/index.js +6 -0
- package/dist/media-understanding/runner.js +158 -90
- package/dist/memory/batch-voyage.js +277 -0
- package/dist/memory/embeddings-voyage.js +75 -0
- package/dist/memory/embeddings.js +28 -16
- package/dist/memory/internal.js +101 -18
- package/dist/memory/manager.js +154 -48
- package/dist/memory/search-manager.js +173 -0
- package/dist/memory/session-files.js +9 -3
- package/dist/node-host/runner.js +34 -24
- package/dist/node-host/with-timeout.js +27 -0
- package/dist/plugins/commands.js +5 -1
- package/dist/plugins/config-state.js +86 -7
- package/dist/plugins/source-display.js +51 -0
- package/dist/process/exec.js +20 -2
- package/dist/routing/resolve-route.js +12 -0
- package/dist/routing/session-key.js +15 -0
- package/dist/runtime.js +2 -0
- package/dist/security/audit-extra.async.js +601 -0
- package/dist/security/audit-extra.js +2 -830
- package/dist/security/audit-extra.sync.js +505 -0
- package/dist/security/channel-metadata.js +34 -0
- package/dist/security/external-content.js +88 -6
- package/dist/security/skill-scanner.js +330 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/signal/monitor/event-handler.js +80 -1
- package/dist/slack/monitor/media.js +85 -15
- package/dist/tailscale/detect.js +1 -2
- package/dist/telegram/bot/helpers.js +109 -28
- package/dist/telegram/bot-handlers.js +144 -3
- package/dist/telegram/bot-message-context.js +37 -10
- package/dist/telegram/bot-message-dispatch.js +54 -17
- package/dist/telegram/bot-native-commands.js +86 -29
- package/dist/telegram/bot.js +30 -29
- package/dist/telegram/model-buttons.js +163 -0
- package/dist/telegram/monitor.js +110 -85
- package/dist/telegram/send.js +129 -47
- package/dist/terminal/restore.js +45 -0
- package/dist/test-helpers/state-dir-env.js +16 -0
- package/dist/tts/tts.js +12 -6
- package/dist/tui/tui-session-actions.js +166 -54
- package/dist/utils/fetch-timeout.js +20 -0
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/utils/transcript-tools.js +58 -0
- package/dist/utils.js +45 -14
- package/dist/version.js +42 -5
- package/dist/wizard/clack-prompter.js +9 -6
- package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
- package/extensions/googlechat/package.json +2 -2
- package/extensions/line/node_modules/.bin/poolbot +21 -0
- package/extensions/line/package.json +1 -1
- package/extensions/matrix/node_modules/.bin/poolbot +21 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
- package/extensions/memory-core/package.json +4 -1
- package/extensions/twitch/node_modules/.bin/poolbot +21 -0
- package/extensions/twitch/package.json +1 -1
- package/package.json +183 -24
- package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
|
@@ -23,7 +23,18 @@ function enhanceBrowserFetchError(url, err, timeoutMs) {
|
|
|
23
23
|
async function fetchHttpJson(url, init) {
|
|
24
24
|
const timeoutMs = init.timeoutMs ?? 5000;
|
|
25
25
|
const ctrl = new AbortController();
|
|
26
|
-
const
|
|
26
|
+
const upstreamSignal = init.signal;
|
|
27
|
+
let upstreamAbortListener;
|
|
28
|
+
if (upstreamSignal) {
|
|
29
|
+
if (upstreamSignal.aborted) {
|
|
30
|
+
ctrl.abort(upstreamSignal.reason);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
|
|
34
|
+
upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
|
|
27
38
|
try {
|
|
28
39
|
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
29
40
|
if (!res.ok) {
|
|
@@ -34,6 +45,9 @@ async function fetchHttpJson(url, init) {
|
|
|
34
45
|
}
|
|
35
46
|
finally {
|
|
36
47
|
clearTimeout(t);
|
|
48
|
+
if (upstreamSignal && upstreamAbortListener) {
|
|
49
|
+
upstreamSignal.removeEventListener("abort", upstreamAbortListener);
|
|
50
|
+
}
|
|
37
51
|
}
|
|
38
52
|
}
|
|
39
53
|
export async function fetchBrowserJson(url, init) {
|
|
@@ -61,6 +75,29 @@ export async function fetchBrowserJson(url, init) {
|
|
|
61
75
|
// keep as string
|
|
62
76
|
}
|
|
63
77
|
}
|
|
78
|
+
const abortCtrl = new AbortController();
|
|
79
|
+
const upstreamSignal = init?.signal;
|
|
80
|
+
let upstreamAbortListener;
|
|
81
|
+
if (upstreamSignal) {
|
|
82
|
+
if (upstreamSignal.aborted) {
|
|
83
|
+
abortCtrl.abort(upstreamSignal.reason);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
|
|
87
|
+
upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let abortListener;
|
|
91
|
+
const abortPromise = abortCtrl.signal.aborted
|
|
92
|
+
? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted"))
|
|
93
|
+
: new Promise((_, reject) => {
|
|
94
|
+
abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
|
|
95
|
+
abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
|
|
96
|
+
});
|
|
97
|
+
let timer;
|
|
98
|
+
if (timeoutMs) {
|
|
99
|
+
timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
|
|
100
|
+
}
|
|
64
101
|
const dispatchPromise = dispatcher.dispatch({
|
|
65
102
|
method: init?.method?.toUpperCase() === "DELETE"
|
|
66
103
|
? "DELETE"
|
|
@@ -70,13 +107,19 @@ export async function fetchBrowserJson(url, init) {
|
|
|
70
107
|
path: parsed.pathname,
|
|
71
108
|
query,
|
|
72
109
|
body,
|
|
110
|
+
signal: abortCtrl.signal,
|
|
111
|
+
});
|
|
112
|
+
const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
|
|
113
|
+
if (timer) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
if (abortListener) {
|
|
117
|
+
abortCtrl.signal.removeEventListener("abort", abortListener);
|
|
118
|
+
}
|
|
119
|
+
if (upstreamSignal && upstreamAbortListener) {
|
|
120
|
+
upstreamSignal.removeEventListener("abort", upstreamAbortListener);
|
|
121
|
+
}
|
|
73
122
|
});
|
|
74
|
-
const result = await (timeoutMs
|
|
75
|
-
? Promise.race([
|
|
76
|
-
dispatchPromise,
|
|
77
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out")), timeoutMs)),
|
|
78
|
-
])
|
|
79
|
-
: dispatchPromise);
|
|
80
123
|
if (result.status >= 400) {
|
|
81
124
|
const message = result.body && typeof result.body === "object" && "error" in result.body
|
|
82
125
|
? String(result.body.error)
|
package/dist/browser/config.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { deriveDefaultBrowserCdpPortRange, deriveDefaultBrowserControlPort, DEFAULT_BROWSER_CONTROL_PORT, } from "../config/port-defaults.js";
|
|
2
2
|
import { resolveGatewayPort } from "../config/paths.js";
|
|
3
|
+
import { isLoopbackHost } from "../gateway/net.js";
|
|
3
4
|
import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_ENABLED, DEFAULT_BROWSER_EVALUATE_ENABLED, DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, DEFAULT_CLAWD_BROWSER_PROFILE_NAME, } from "./constants.js";
|
|
4
5
|
import { CDP_PORT_RANGE_START, getUsedPorts } from "./profiles.js";
|
|
5
|
-
function isLoopbackHost(host) {
|
|
6
|
-
const h = host.trim().toLowerCase();
|
|
7
|
-
return (h === "localhost" ||
|
|
8
|
-
h === "127.0.0.1" ||
|
|
9
|
-
h === "0.0.0.0" ||
|
|
10
|
-
h === "[::1]" ||
|
|
11
|
-
h === "::1" ||
|
|
12
|
-
h === "[::]" ||
|
|
13
|
-
h === "::");
|
|
14
|
-
}
|
|
15
6
|
function normalizeHexColor(raw) {
|
|
16
7
|
const value = (raw ?? "").trim();
|
|
17
8
|
if (!value)
|
|
@@ -1,28 +1,20 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { createServer } from "node:http";
|
|
2
3
|
import WebSocket, { WebSocketServer } from "ws";
|
|
4
|
+
import { isLoopbackAddress, isLoopbackHost } from "../gateway/net.js";
|
|
3
5
|
import { rawDataToString } from "../infra/ws.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const RELAY_AUTH_HEADER = "x-poolbot-relay-token";
|
|
7
|
+
function headerValue(value) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value[0];
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
13
15
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
return false;
|
|
17
|
-
if (ip === "127.0.0.1")
|
|
18
|
-
return true;
|
|
19
|
-
if (ip.startsWith("127."))
|
|
20
|
-
return true;
|
|
21
|
-
if (ip === "::1")
|
|
22
|
-
return true;
|
|
23
|
-
if (ip.startsWith("::ffff:127."))
|
|
24
|
-
return true;
|
|
25
|
-
return false;
|
|
16
|
+
function getHeader(req, name) {
|
|
17
|
+
return headerValue(req.headers[name.toLowerCase()]);
|
|
26
18
|
}
|
|
27
19
|
function parseBaseUrl(raw) {
|
|
28
20
|
const parsed = new URL(raw.trim().replace(/\/$/, ""));
|
|
@@ -56,14 +48,43 @@ function rejectUpgrade(socket, status, bodyText) {
|
|
|
56
48
|
}
|
|
57
49
|
}
|
|
58
50
|
const serversByPort = new Map();
|
|
51
|
+
const relayAuthByPort = new Map();
|
|
52
|
+
function relayAuthTokenForUrl(url) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(url);
|
|
55
|
+
if (!isLoopbackHost(parsed.hostname)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const port = parsed.port?.trim() !== ""
|
|
59
|
+
? Number(parsed.port)
|
|
60
|
+
: parsed.protocol === "https:" || parsed.protocol === "wss:"
|
|
61
|
+
? 443
|
|
62
|
+
: 80;
|
|
63
|
+
if (!Number.isFinite(port)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return relayAuthByPort.get(port) ?? null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function getChromeExtensionRelayAuthHeaders(url) {
|
|
73
|
+
const token = relayAuthTokenForUrl(url);
|
|
74
|
+
if (!token) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
return { [RELAY_AUTH_HEADER]: token };
|
|
78
|
+
}
|
|
59
79
|
export async function ensureChromeExtensionRelayServer(opts) {
|
|
60
80
|
const info = parseBaseUrl(opts.cdpUrl);
|
|
61
81
|
if (!isLoopbackHost(info.host)) {
|
|
62
82
|
throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
|
|
63
83
|
}
|
|
64
84
|
const existing = serversByPort.get(info.port);
|
|
65
|
-
if (existing)
|
|
85
|
+
if (existing) {
|
|
66
86
|
return existing;
|
|
87
|
+
}
|
|
67
88
|
let extensionWs = null;
|
|
68
89
|
const cdpClients = new Set();
|
|
69
90
|
const connectedTargets = new Map();
|
|
@@ -86,14 +107,16 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
86
107
|
const broadcastToCdpClients = (evt) => {
|
|
87
108
|
const msg = JSON.stringify(evt);
|
|
88
109
|
for (const ws of cdpClients) {
|
|
89
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
110
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
90
111
|
continue;
|
|
112
|
+
}
|
|
91
113
|
ws.send(msg);
|
|
92
114
|
}
|
|
93
115
|
};
|
|
94
116
|
const sendResponseToCdp = (ws, res) => {
|
|
95
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
117
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
96
118
|
return;
|
|
119
|
+
}
|
|
97
120
|
ws.send(JSON.stringify(res));
|
|
98
121
|
};
|
|
99
122
|
const ensureTargetEventsForClient = (ws, mode) => {
|
|
@@ -143,14 +166,16 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
143
166
|
const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
|
|
144
167
|
if (targetId) {
|
|
145
168
|
for (const t of connectedTargets.values()) {
|
|
146
|
-
if (t.targetId === targetId)
|
|
169
|
+
if (t.targetId === targetId) {
|
|
147
170
|
return { targetInfo: t.targetInfo };
|
|
171
|
+
}
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
|
|
151
175
|
const t = connectedTargets.get(cmd.sessionId);
|
|
152
|
-
if (t)
|
|
176
|
+
if (t) {
|
|
153
177
|
return { targetInfo: t.targetInfo };
|
|
178
|
+
}
|
|
154
179
|
}
|
|
155
180
|
const first = Array.from(connectedTargets.values())[0];
|
|
156
181
|
return { targetInfo: first?.targetInfo };
|
|
@@ -158,11 +183,13 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
158
183
|
case "Target.attachToTarget": {
|
|
159
184
|
const params = (cmd.params ?? {});
|
|
160
185
|
const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
|
|
161
|
-
if (!targetId)
|
|
186
|
+
if (!targetId) {
|
|
162
187
|
throw new Error("targetId required");
|
|
188
|
+
}
|
|
163
189
|
for (const t of connectedTargets.values()) {
|
|
164
|
-
if (t.targetId === targetId)
|
|
190
|
+
if (t.targetId === targetId) {
|
|
165
191
|
return { sessionId: t.sessionId };
|
|
192
|
+
}
|
|
166
193
|
}
|
|
167
194
|
throw new Error("target not found");
|
|
168
195
|
}
|
|
@@ -180,9 +207,18 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
180
207
|
}
|
|
181
208
|
}
|
|
182
209
|
};
|
|
210
|
+
const relayAuthToken = randomBytes(32).toString("base64url");
|
|
183
211
|
const server = createServer((req, res) => {
|
|
184
212
|
const url = new URL(req.url ?? "/", info.baseUrl);
|
|
185
213
|
const path = url.pathname;
|
|
214
|
+
if (path.startsWith("/json")) {
|
|
215
|
+
const token = getHeader(req, RELAY_AUTH_HEADER);
|
|
216
|
+
if (!token || token !== relayAuthToken) {
|
|
217
|
+
res.writeHead(401);
|
|
218
|
+
res.end("Unauthorized");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
186
222
|
if (req.method === "HEAD" && path === "/") {
|
|
187
223
|
res.writeHead(200);
|
|
188
224
|
res.end();
|
|
@@ -208,8 +244,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
208
244
|
"Protocol-Version": "1.3",
|
|
209
245
|
};
|
|
210
246
|
// Only advertise the WS URL if a real extension is connected.
|
|
211
|
-
if (extensionWs)
|
|
247
|
+
if (extensionWs) {
|
|
212
248
|
payload.webSocketDebuggerUrl = cdpWsUrl;
|
|
249
|
+
}
|
|
213
250
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
214
251
|
res.end(JSON.stringify(payload));
|
|
215
252
|
return;
|
|
@@ -290,6 +327,11 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
290
327
|
rejectUpgrade(socket, 403, "Forbidden");
|
|
291
328
|
return;
|
|
292
329
|
}
|
|
330
|
+
const origin = headerValue(req.headers.origin);
|
|
331
|
+
if (origin && !origin.startsWith("chrome-extension://")) {
|
|
332
|
+
rejectUpgrade(socket, 403, "Forbidden: invalid origin");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
293
335
|
if (pathname === "/extension") {
|
|
294
336
|
if (extensionWs) {
|
|
295
337
|
rejectUpgrade(socket, 409, "Extension already connected");
|
|
@@ -301,6 +343,11 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
301
343
|
return;
|
|
302
344
|
}
|
|
303
345
|
if (pathname === "/cdp") {
|
|
346
|
+
const token = getHeader(req, RELAY_AUTH_HEADER);
|
|
347
|
+
if (!token || token !== relayAuthToken) {
|
|
348
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
304
351
|
if (!extensionWs) {
|
|
305
352
|
rejectUpgrade(socket, 503, "Extension not connected");
|
|
306
353
|
return;
|
|
@@ -315,8 +362,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
315
362
|
wssExtension.on("connection", (ws) => {
|
|
316
363
|
extensionWs = ws;
|
|
317
364
|
const ping = setInterval(() => {
|
|
318
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
365
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
319
366
|
return;
|
|
367
|
+
}
|
|
320
368
|
ws.send(JSON.stringify({ method: "ping" }));
|
|
321
369
|
}, 5000);
|
|
322
370
|
ws.on("message", (data) => {
|
|
@@ -329,8 +377,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
329
377
|
}
|
|
330
378
|
if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
|
|
331
379
|
const pending = pendingExtension.get(parsed.id);
|
|
332
|
-
if (!pending)
|
|
380
|
+
if (!pending) {
|
|
333
381
|
return;
|
|
382
|
+
}
|
|
334
383
|
pendingExtension.delete(parsed.id);
|
|
335
384
|
clearTimeout(pending.timer);
|
|
336
385
|
if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) {
|
|
@@ -342,21 +391,25 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
342
391
|
return;
|
|
343
392
|
}
|
|
344
393
|
if (parsed && typeof parsed === "object" && "method" in parsed) {
|
|
345
|
-
if (parsed.method === "pong")
|
|
394
|
+
if (parsed.method === "pong") {
|
|
346
395
|
return;
|
|
347
|
-
|
|
396
|
+
}
|
|
397
|
+
if (parsed.method !== "forwardCDPEvent") {
|
|
348
398
|
return;
|
|
399
|
+
}
|
|
349
400
|
const evt = parsed;
|
|
350
401
|
const method = evt.params?.method;
|
|
351
402
|
const params = evt.params?.params;
|
|
352
403
|
const sessionId = evt.params?.sessionId;
|
|
353
|
-
if (!method || typeof method !== "string")
|
|
404
|
+
if (!method || typeof method !== "string") {
|
|
354
405
|
return;
|
|
406
|
+
}
|
|
355
407
|
if (method === "Target.attachedToTarget") {
|
|
356
408
|
const attached = (params ?? {});
|
|
357
409
|
const targetType = attached?.targetInfo?.type ?? "page";
|
|
358
|
-
if (targetType !== "page")
|
|
410
|
+
if (targetType !== "page") {
|
|
359
411
|
return;
|
|
412
|
+
}
|
|
360
413
|
if (attached?.sessionId && attached?.targetInfo?.targetId) {
|
|
361
414
|
const prev = connectedTargets.get(attached.sessionId);
|
|
362
415
|
const nextTargetId = attached.targetInfo.targetId;
|
|
@@ -382,8 +435,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
382
435
|
}
|
|
383
436
|
if (method === "Target.detachedFromTarget") {
|
|
384
437
|
const detached = (params ?? {});
|
|
385
|
-
if (detached?.sessionId)
|
|
438
|
+
if (detached?.sessionId) {
|
|
386
439
|
connectedTargets.delete(detached.sessionId);
|
|
440
|
+
}
|
|
387
441
|
broadcastToCdpClients({ method, params, sessionId });
|
|
388
442
|
return;
|
|
389
443
|
}
|
|
@@ -395,8 +449,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
395
449
|
const targetId = targetInfo?.targetId;
|
|
396
450
|
if (targetId && (targetInfo?.type ?? "page") === "page") {
|
|
397
451
|
for (const [sid, target] of connectedTargets) {
|
|
398
|
-
if (target.targetId !== targetId)
|
|
452
|
+
if (target.targetId !== targetId) {
|
|
399
453
|
continue;
|
|
454
|
+
}
|
|
400
455
|
connectedTargets.set(sid, {
|
|
401
456
|
...target,
|
|
402
457
|
targetInfo: { ...target.targetInfo, ...targetInfo },
|
|
@@ -437,10 +492,12 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
437
492
|
catch {
|
|
438
493
|
return;
|
|
439
494
|
}
|
|
440
|
-
if (!cmd || typeof cmd !== "object")
|
|
495
|
+
if (!cmd || typeof cmd !== "object") {
|
|
441
496
|
return;
|
|
442
|
-
|
|
497
|
+
}
|
|
498
|
+
if (typeof cmd.id !== "number" || typeof cmd.method !== "string") {
|
|
443
499
|
return;
|
|
500
|
+
}
|
|
444
501
|
if (!extensionWs) {
|
|
445
502
|
sendResponseToCdp(ws, {
|
|
446
503
|
id: cmd.id,
|
|
@@ -507,6 +564,7 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
507
564
|
extensionConnected: () => Boolean(extensionWs),
|
|
508
565
|
stop: async () => {
|
|
509
566
|
serversByPort.delete(port);
|
|
567
|
+
relayAuthByPort.delete(port);
|
|
510
568
|
try {
|
|
511
569
|
extensionWs?.close(1001, "server stopping");
|
|
512
570
|
}
|
|
@@ -528,14 +586,17 @@ export async function ensureChromeExtensionRelayServer(opts) {
|
|
|
528
586
|
wssCdp.close();
|
|
529
587
|
},
|
|
530
588
|
};
|
|
589
|
+
relayAuthByPort.set(port, relayAuthToken);
|
|
531
590
|
serversByPort.set(port, relay);
|
|
532
591
|
return relay;
|
|
533
592
|
}
|
|
534
593
|
export async function stopChromeExtensionRelayServer(opts) {
|
|
535
594
|
const info = parseBaseUrl(opts.cdpUrl);
|
|
536
595
|
const existing = serversByPort.get(info.port);
|
|
537
|
-
if (!existing)
|
|
596
|
+
if (!existing) {
|
|
538
597
|
return false;
|
|
598
|
+
}
|
|
539
599
|
await existing.stop();
|
|
600
|
+
relayAuthByPort.delete(info.port);
|
|
540
601
|
return true;
|
|
541
602
|
}
|
package/dist/browser/pw-ai.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { closePageByTargetIdViaPlaywright, closePlaywrightBrowserConnection, createPageViaPlaywright, ensurePageState, focusPageByTargetIdViaPlaywright, getPageForTargetId, listPagesViaPlaywright, refLocator, } from "./pw-session.js";
|
|
1
|
+
export { closePageByTargetIdViaPlaywright, closePlaywrightBrowserConnection, createPageViaPlaywright, ensurePageState, forceDisconnectPlaywrightForTarget, focusPageByTargetIdViaPlaywright, getPageForTargetId, listPagesViaPlaywright, refLocator, } from "./pw-session.js";
|
|
2
2
|
export { armDialogViaPlaywright, armFileUploadViaPlaywright, clickViaPlaywright, closePageViaPlaywright, cookiesClearViaPlaywright, cookiesGetViaPlaywright, cookiesSetViaPlaywright, downloadViaPlaywright, dragViaPlaywright, emulateMediaViaPlaywright, evaluateViaPlaywright, fillFormViaPlaywright, getConsoleMessagesViaPlaywright, getNetworkRequestsViaPlaywright, getPageErrorsViaPlaywright, highlightViaPlaywright, hoverViaPlaywright, navigateViaPlaywright, pdfViaPlaywright, pressKeyViaPlaywright, resizeViewportViaPlaywright, responseBodyViaPlaywright, scrollIntoViewViaPlaywright, selectOptionViaPlaywright, setDeviceViaPlaywright, setExtraHTTPHeadersViaPlaywright, setGeolocationViaPlaywright, setHttpCredentialsViaPlaywright, setInputFilesViaPlaywright, setLocaleViaPlaywright, setOfflineViaPlaywright, setTimezoneViaPlaywright, snapshotAiViaPlaywright, snapshotAriaViaPlaywright, snapshotRoleViaPlaywright, screenshotWithLabelsViaPlaywright, storageClearViaPlaywright, storageGetViaPlaywright, storageSetViaPlaywright, takeScreenshotViaPlaywright, traceStartViaPlaywright, traceStopViaPlaywright, typeViaPlaywright, waitForDownloadViaPlaywright, waitForViaPlaywright, } from "./pw-tools-core.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chromium } from "playwright-core";
|
|
2
2
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
3
|
-
import { getHeadersWithAuth } from "./cdp.helpers.js";
|
|
3
|
+
import { appendCdpPath, fetchJson, getHeadersWithAuth, withCdpSocket } from "./cdp.helpers.js";
|
|
4
|
+
import { normalizeCdpWsUrl } from "./cdp.js";
|
|
4
5
|
import { getChromeWebSocketUrl } from "./chrome.js";
|
|
5
6
|
const pageStates = new WeakMap();
|
|
6
7
|
const contextStates = new WeakMap();
|
|
@@ -196,13 +197,15 @@ async function connectBrowser(cdpUrl) {
|
|
|
196
197
|
const endpoint = wsUrl ?? normalized;
|
|
197
198
|
const headers = getHeadersWithAuth(endpoint);
|
|
198
199
|
const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
|
|
199
|
-
const
|
|
200
|
+
const onDisconnected = () => {
|
|
201
|
+
if (cached?.browser === browser) {
|
|
202
|
+
cached = null;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const connected = { browser, cdpUrl: normalized, onDisconnected };
|
|
200
206
|
cached = connected;
|
|
207
|
+
browser.on("disconnected", onDisconnected);
|
|
201
208
|
observeBrowser(browser);
|
|
202
|
-
browser.on("disconnected", () => {
|
|
203
|
-
if (cached?.browser === browser)
|
|
204
|
-
cached = null;
|
|
205
|
-
});
|
|
206
209
|
return connected;
|
|
207
210
|
}
|
|
208
211
|
catch (err) {
|
|
@@ -254,7 +257,8 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
254
257
|
.replace(/\/+$/, "")
|
|
255
258
|
.replace(/^ws:/, "http:")
|
|
256
259
|
.replace(/\/cdp$/, "");
|
|
257
|
-
const
|
|
260
|
+
const listUrl = `${baseUrl}/json/list`;
|
|
261
|
+
const response = await fetch(listUrl, { headers: getHeadersWithAuth(listUrl) });
|
|
258
262
|
if (response.ok) {
|
|
259
263
|
const targets = (await response.json());
|
|
260
264
|
const target = targets.find((t) => t.id === targetId);
|
|
@@ -335,10 +339,141 @@ export function refLocator(page, ref) {
|
|
|
335
339
|
export async function closePlaywrightBrowserConnection() {
|
|
336
340
|
const cur = cached;
|
|
337
341
|
cached = null;
|
|
338
|
-
|
|
342
|
+
connecting = null;
|
|
343
|
+
if (!cur) {
|
|
339
344
|
return;
|
|
345
|
+
}
|
|
346
|
+
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
347
|
+
cur.browser.off("disconnected", cur.onDisconnected);
|
|
348
|
+
}
|
|
340
349
|
await cur.browser.close().catch(() => { });
|
|
341
350
|
}
|
|
351
|
+
function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) {
|
|
352
|
+
try {
|
|
353
|
+
const url = new URL(cdpUrl);
|
|
354
|
+
if (url.protocol === "ws:") {
|
|
355
|
+
url.protocol = "http:";
|
|
356
|
+
}
|
|
357
|
+
else if (url.protocol === "wss:") {
|
|
358
|
+
url.protocol = "https:";
|
|
359
|
+
}
|
|
360
|
+
url.pathname = url.pathname.replace(/\/devtools\/browser\/.*$/, "");
|
|
361
|
+
url.pathname = url.pathname.replace(/\/cdp$/, "");
|
|
362
|
+
return url.toString().replace(/\/$/, "");
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Best-effort fallback for non-URL-ish inputs.
|
|
366
|
+
return cdpUrl
|
|
367
|
+
.replace(/^ws:/, "http:")
|
|
368
|
+
.replace(/^wss:/, "https:")
|
|
369
|
+
.replace(/\/devtools\/browser\/.*$/, "")
|
|
370
|
+
.replace(/\/cdp$/, "")
|
|
371
|
+
.replace(/\/$/, "");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function cdpSocketNeedsAttach(wsUrl) {
|
|
375
|
+
try {
|
|
376
|
+
const pathname = new URL(wsUrl).pathname;
|
|
377
|
+
return (pathname === "/cdp" || pathname.endsWith("/cdp") || pathname.includes("/devtools/browser/"));
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function tryTerminateExecutionViaCdp(opts) {
|
|
384
|
+
const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(opts.cdpUrl);
|
|
385
|
+
const listUrl = appendCdpPath(cdpHttpBase, "/json/list");
|
|
386
|
+
const pages = await fetchJson(listUrl, 2000).catch(() => null);
|
|
387
|
+
if (!pages || pages.length === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const target = pages.find((p) => String(p.id ?? "").trim() === opts.targetId);
|
|
391
|
+
const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
|
|
392
|
+
if (!wsUrlRaw) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, cdpHttpBase);
|
|
396
|
+
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
397
|
+
const runWithTimeout = async (work, ms) => {
|
|
398
|
+
let timer;
|
|
399
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
400
|
+
timer = setTimeout(() => reject(new Error("CDP command timed out")), ms);
|
|
401
|
+
});
|
|
402
|
+
try {
|
|
403
|
+
return await Promise.race([work, timeoutPromise]);
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
if (timer) {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
await withCdpSocket(wsUrl, async (send) => {
|
|
412
|
+
let sessionId;
|
|
413
|
+
try {
|
|
414
|
+
if (needsAttach) {
|
|
415
|
+
const attached = (await runWithTimeout(send("Target.attachToTarget", { targetId: opts.targetId, flatten: true }), 1500));
|
|
416
|
+
if (typeof attached?.sessionId === "string" && attached.sessionId.trim()) {
|
|
417
|
+
sessionId = attached.sessionId;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
await runWithTimeout(send("Runtime.terminateExecution", undefined, sessionId), 1500);
|
|
421
|
+
if (sessionId) {
|
|
422
|
+
// Best-effort cleanup; not required for termination to take effect.
|
|
423
|
+
void send("Target.detachFromTarget", { sessionId }).catch(() => { });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Best-effort; ignore
|
|
428
|
+
}
|
|
429
|
+
}, { handshakeTimeoutMs: 2000 }).catch(() => { });
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Best-effort cancellation for stuck page operations.
|
|
433
|
+
*
|
|
434
|
+
* Playwright serializes CDP commands per page; a long-running or stuck operation (notably evaluate)
|
|
435
|
+
* can block all subsequent commands. We cannot safely "cancel" an individual command, and we do
|
|
436
|
+
* not want to close the actual Chromium tab. Instead, we disconnect Playwright's CDP connection
|
|
437
|
+
* so in-flight commands fail fast and the next request reconnects transparently.
|
|
438
|
+
*
|
|
439
|
+
* IMPORTANT: We CANNOT call Connection.close() because Playwright shares a single Connection
|
|
440
|
+
* across all objects (BrowserType, Browser, etc.). Closing it corrupts the entire Playwright
|
|
441
|
+
* instance, preventing reconnection.
|
|
442
|
+
*
|
|
443
|
+
* Instead we:
|
|
444
|
+
* 1. Null out `cached` so the next call triggers a fresh connectOverCDP
|
|
445
|
+
* 2. Fire-and-forget browser.close() — it may hang but won't block us
|
|
446
|
+
* 3. The next connectBrowser() creates a completely new CDP WebSocket connection
|
|
447
|
+
*
|
|
448
|
+
* The old browser.close() eventually resolves when the in-browser evaluate timeout fires,
|
|
449
|
+
* or the old connection gets GC'd. Either way, it doesn't affect the fresh connection.
|
|
450
|
+
*/
|
|
451
|
+
export async function forceDisconnectPlaywrightForTarget(opts) {
|
|
452
|
+
const normalized = normalizeCdpUrl(opts.cdpUrl);
|
|
453
|
+
if (cached?.cdpUrl !== normalized) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const cur = cached;
|
|
457
|
+
cached = null;
|
|
458
|
+
// Also clear `connecting` so the next call does a fresh connectOverCDP
|
|
459
|
+
// rather than awaiting a stale promise.
|
|
460
|
+
connecting = null;
|
|
461
|
+
if (cur) {
|
|
462
|
+
// Remove the "disconnected" listener to prevent the old browser's teardown
|
|
463
|
+
// from racing with a fresh connection and nulling the new `cached`.
|
|
464
|
+
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
465
|
+
cur.browser.off("disconnected", cur.onDisconnected);
|
|
466
|
+
}
|
|
467
|
+
// Best-effort: kill any stuck JS to unblock the target's execution context before we
|
|
468
|
+
// disconnect Playwright's CDP connection.
|
|
469
|
+
const targetId = opts.targetId?.trim() || "";
|
|
470
|
+
if (targetId) {
|
|
471
|
+
await tryTerminateExecutionViaCdp({ cdpUrl: normalized, targetId }).catch(() => { });
|
|
472
|
+
}
|
|
473
|
+
// Fire-and-forget: don't await because browser.close() may hang on the stuck CDP pipe.
|
|
474
|
+
cur.browser.close().catch(() => { });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
342
477
|
/**
|
|
343
478
|
* List all pages/tabs from the persistent Playwright connection.
|
|
344
479
|
* Used for remote profiles where HTTP-based /json/list is ephemeral.
|