@reconcrap/boss-recommend-mcp 2.0.50 → 2.0.51
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/package.json +1 -1
- package/src/cli.js +68 -44
- package/src/core/browser/index.js +629 -23
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,10 +7,10 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import {
|
|
9
9
|
assertNoForbiddenCdpCalls,
|
|
10
|
-
buildBossChromeLaunchArgs,
|
|
11
10
|
bringPageToFront,
|
|
12
11
|
connectToChromeTarget,
|
|
13
12
|
enableDomains,
|
|
13
|
+
ensureChromeDebugPort,
|
|
14
14
|
getDocumentRoot,
|
|
15
15
|
querySelector,
|
|
16
16
|
sleep as sleepMs
|
|
@@ -2209,51 +2209,38 @@ async function launchChrome(options = {}) {
|
|
|
2209
2209
|
const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
|
|
2210
2210
|
process.env.BOSS_RECOMMEND_CHROME_PORT = String(port);
|
|
2211
2211
|
const timing = getLaunchChromeTiming(options);
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
inspectTimeoutMs: timing.inspectTimeoutMs,
|
|
2222
|
-
pollMs: timing.pollMs,
|
|
2223
|
-
settleMs: timing.settleMs
|
|
2212
|
+
const userDataDir = getChromeUserDataDir(port);
|
|
2213
|
+
let chromeGuard = null;
|
|
2214
|
+
try {
|
|
2215
|
+
chromeGuard = await ensureChromeDebugPort({
|
|
2216
|
+
port,
|
|
2217
|
+
url: bossUrl,
|
|
2218
|
+
slowLive: Boolean(options["slow-live"] || options.slowLive),
|
|
2219
|
+
launchIfMissing: true,
|
|
2220
|
+
userDataDir
|
|
2224
2221
|
});
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
console.log(`CDP methods: ${frontResult.method_log.join(", ") || "none"}`);
|
|
2230
|
-
}
|
|
2231
|
-
} else {
|
|
2232
|
-
console.log(pageState.page_state?.message || "Boss recommend page is not ready.");
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
console.error(error?.message || String(error || "Chrome launch failed"));
|
|
2224
|
+
if (error?.chrome_guard) {
|
|
2225
|
+
console.error(JSON.stringify(error.chrome_guard, null, 2));
|
|
2233
2226
|
}
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
const chromePath = getChromeExecutable();
|
|
2238
|
-
if (!chromePath) {
|
|
2239
|
-
console.error("Chrome executable not found. Set BOSS_RECOMMEND_CHROME_PATH or install Google Chrome.");
|
|
2240
2227
|
process.exitCode = 1;
|
|
2241
2228
|
return;
|
|
2242
2229
|
}
|
|
2243
2230
|
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2231
|
+
if (chromeGuard.replaced) {
|
|
2232
|
+
console.log(`Replaced Chrome debug instance on port ${port} because required flags were missing: ${chromeGuard.missing_flags.join(", ")}`);
|
|
2233
|
+
} else if (chromeGuard.launched) {
|
|
2234
|
+
console.log(`Chrome launched with remote debugging port ${port}`);
|
|
2235
|
+
} else {
|
|
2236
|
+
console.log(`Reusing existing Chrome debug instance on port ${port} with required flags`);
|
|
2237
|
+
}
|
|
2238
|
+
console.log(`User data dir: ${chromeGuard.user_data_dir || userDataDir}`);
|
|
2239
|
+
if (chromeGuard.launched || chromeGuard.replaced) {
|
|
2240
|
+
await sleepMs(timing.settleMs + 1200);
|
|
2241
|
+
}
|
|
2255
2242
|
const pageState = await ensureBossRecommendPageReadyCdp(port, {
|
|
2256
|
-
attempts: 6,
|
|
2243
|
+
attempts: chromeGuard.launched || chromeGuard.replaced ? 6 : 2,
|
|
2257
2244
|
inspectTimeoutMs: timing.inspectTimeoutMs,
|
|
2258
2245
|
pollMs: timing.pollMs,
|
|
2259
2246
|
settleMs: timing.settleMs
|
|
@@ -2382,15 +2369,30 @@ async function printDoctor(options = {}) {
|
|
|
2382
2369
|
const configResolution = getBossScreenConfigResolution(workspaceRoot);
|
|
2383
2370
|
const calibrationResolution = getFeaturedCalibrationResolutionLocal(workspaceRoot);
|
|
2384
2371
|
const timing = getLaunchChromeTiming(options);
|
|
2372
|
+
const slowLive = Boolean(options["slow-live"] || options.slowLive);
|
|
2373
|
+
let chromeGuard = null;
|
|
2374
|
+
let chromeGuardError = null;
|
|
2375
|
+
try {
|
|
2376
|
+
chromeGuard = await ensureChromeDebugPort({
|
|
2377
|
+
port,
|
|
2378
|
+
url: bossUrl,
|
|
2379
|
+
slowLive,
|
|
2380
|
+
launchIfMissing: true,
|
|
2381
|
+
userDataDir: getChromeUserDataDir(port)
|
|
2382
|
+
});
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
chromeGuardError = error;
|
|
2385
|
+
chromeGuard = error?.chrome_guard || null;
|
|
2386
|
+
}
|
|
2385
2387
|
let pageState = await inspectBossRecommendPageStateCdp(port, {
|
|
2386
|
-
timeoutMs:
|
|
2387
|
-
pollMs:
|
|
2388
|
+
timeoutMs: slowLive ? timing.initialTimeoutMs : 2000,
|
|
2389
|
+
pollMs: slowLive ? timing.pollMs : 500
|
|
2388
2390
|
});
|
|
2389
2391
|
if (pageState.state === "RECOMMEND_READY") {
|
|
2390
2392
|
pageState = await verifyRecommendPageStableCdp(port, {
|
|
2391
|
-
settleMs:
|
|
2392
|
-
recheckTimeoutMs:
|
|
2393
|
-
pollMs:
|
|
2393
|
+
settleMs: slowLive ? timing.settleMs : 800,
|
|
2394
|
+
recheckTimeoutMs: slowLive ? timing.inspectTimeoutMs : 3000,
|
|
2395
|
+
pollMs: slowLive ? timing.pollMs : 500
|
|
2394
2396
|
});
|
|
2395
2397
|
}
|
|
2396
2398
|
const resolvedConfigPath = configResolution.resolved_path || configResolution.writable_path;
|
|
@@ -2407,6 +2409,28 @@ async function printDoctor(options = {}) {
|
|
|
2407
2409
|
? `检测到配置文件(resolved_path):${resolvedConfigPath}`
|
|
2408
2410
|
: "用户配置不存在(可通过 `boss-recommend-mcp init-config` 创建模板,或 `boss-recommend-mcp config set` 写入真实值)"
|
|
2409
2411
|
});
|
|
2412
|
+
const requiredFlags = chromeGuard?.required_flags || [];
|
|
2413
|
+
const missingFlags = chromeGuard?.missing_flags || [];
|
|
2414
|
+
const chromeFlagsOk = Boolean(chromeGuard && !chromeGuardError && chromeGuard.required_flags_ok);
|
|
2415
|
+
checks.push({
|
|
2416
|
+
key: "chrome_required_flags",
|
|
2417
|
+
ok: chromeFlagsOk,
|
|
2418
|
+
path: `http://localhost:${port}`,
|
|
2419
|
+
required_flags: requiredFlags,
|
|
2420
|
+
missing_flags: missingFlags,
|
|
2421
|
+
replaced: Boolean(chromeGuard?.replaced),
|
|
2422
|
+
close_method: chromeGuard?.close_method || null,
|
|
2423
|
+
relaunch: chromeGuard?.relaunch || null,
|
|
2424
|
+
message: chromeFlagsOk
|
|
2425
|
+
? chromeGuard?.replaced
|
|
2426
|
+
? `Chrome 调试端口 ${port} 原实例缺少必需 flags,已自动关闭并用正确 flags 重新启动。`
|
|
2427
|
+
: chromeGuard?.launched
|
|
2428
|
+
? `Chrome 调试端口 ${port} 已用必需 flags 启动。`
|
|
2429
|
+
: `Chrome 调试端口 ${port} 已确认包含必需 flags。`
|
|
2430
|
+
: chromeGuardError
|
|
2431
|
+
? `Chrome 必需 flags 检查失败:${chromeGuardError.message}`
|
|
2432
|
+
: `Chrome 调试端口 ${port} 未确认包含必需 flags。`
|
|
2433
|
+
});
|
|
2410
2434
|
checks.push({
|
|
2411
2435
|
key: "chrome_debug_port",
|
|
2412
2436
|
ok: pageState.state !== "DEBUG_PORT_UNREACHABLE",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -13,6 +13,7 @@ export const LID_CLOSED_SAFE_CHROME_ARGS = [
|
|
|
13
13
|
"--disable-renderer-backgrounding",
|
|
14
14
|
"--disable-features=CalculateNativeWinOcclusion"
|
|
15
15
|
];
|
|
16
|
+
export const DEFAULT_REQUIRED_CHROME_FLAGS = LID_CLOSED_SAFE_CHROME_ARGS;
|
|
16
17
|
|
|
17
18
|
export const ALLOWED_CDP_DOMAINS = new Set([
|
|
18
19
|
"Accessibility",
|
|
@@ -625,6 +626,109 @@ function parseExtraChromeArgs(value = "") {
|
|
|
625
626
|
.filter(Boolean);
|
|
626
627
|
}
|
|
627
628
|
|
|
629
|
+
export function parseChromeCommandLineArgs(commandLineOrArgs = []) {
|
|
630
|
+
if (Array.isArray(commandLineOrArgs)) {
|
|
631
|
+
return commandLineOrArgs
|
|
632
|
+
.map((item) => String(item || "").trim())
|
|
633
|
+
.filter(Boolean);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const text = String(commandLineOrArgs || "").trim();
|
|
637
|
+
if (!text) return [];
|
|
638
|
+
const args = [];
|
|
639
|
+
let current = "";
|
|
640
|
+
let quote = null;
|
|
641
|
+
for (const char of text) {
|
|
642
|
+
if (quote) {
|
|
643
|
+
if (char === quote) {
|
|
644
|
+
quote = null;
|
|
645
|
+
} else {
|
|
646
|
+
current += char;
|
|
647
|
+
}
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (char === '"' || char === "'") {
|
|
651
|
+
quote = char;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (/\s/.test(char)) {
|
|
655
|
+
if (current) {
|
|
656
|
+
args.push(current);
|
|
657
|
+
current = "";
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
current += char;
|
|
662
|
+
}
|
|
663
|
+
if (current) args.push(current);
|
|
664
|
+
return args;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function splitChromeFeatureList(value = "") {
|
|
668
|
+
return String(value || "")
|
|
669
|
+
.split(",")
|
|
670
|
+
.map((item) => item.trim())
|
|
671
|
+
.filter(Boolean);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function chromeFlagIsPresent(args, requiredFlag) {
|
|
675
|
+
if (!requiredFlag) return true;
|
|
676
|
+
const disableFeaturesPrefix = "--disable-features=";
|
|
677
|
+
if (requiredFlag.startsWith(disableFeaturesPrefix)) {
|
|
678
|
+
const requiredFeatures = splitChromeFeatureList(requiredFlag.slice(disableFeaturesPrefix.length));
|
|
679
|
+
const disableFeatureArgs = args.filter((arg) => arg.startsWith(disableFeaturesPrefix));
|
|
680
|
+
const lastDisableFeatureArg = disableFeatureArgs[disableFeatureArgs.length - 1] || "";
|
|
681
|
+
const features = splitChromeFeatureList(lastDisableFeatureArg.slice(disableFeaturesPrefix.length));
|
|
682
|
+
return requiredFeatures.every((feature) => features.includes(feature));
|
|
683
|
+
}
|
|
684
|
+
if (args.includes(requiredFlag)) return true;
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function getMissingRequiredChromeFlags(
|
|
689
|
+
commandLineOrArgs = [],
|
|
690
|
+
requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS
|
|
691
|
+
) {
|
|
692
|
+
const args = parseChromeCommandLineArgs(commandLineOrArgs);
|
|
693
|
+
return requiredFlags.filter((flag) => !chromeFlagIsPresent(args, flag));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function normalizeChromeLaunchArgs(args = []) {
|
|
697
|
+
const disableFeaturesPrefix = "--disable-features=";
|
|
698
|
+
const result = [];
|
|
699
|
+
const seen = new Set();
|
|
700
|
+
const disabledFeatures = [];
|
|
701
|
+
const disabledFeatureSet = new Set();
|
|
702
|
+
let disabledFeatureIndex = -1;
|
|
703
|
+
|
|
704
|
+
for (const rawArg of args) {
|
|
705
|
+
const arg = String(rawArg || "").trim();
|
|
706
|
+
if (!arg) continue;
|
|
707
|
+
if (arg.startsWith(disableFeaturesPrefix)) {
|
|
708
|
+
if (disabledFeatureIndex < 0) {
|
|
709
|
+
disabledFeatureIndex = result.length;
|
|
710
|
+
result.push(null);
|
|
711
|
+
}
|
|
712
|
+
for (const feature of splitChromeFeatureList(arg.slice(disableFeaturesPrefix.length))) {
|
|
713
|
+
if (!disabledFeatureSet.has(feature)) {
|
|
714
|
+
disabledFeatureSet.add(feature);
|
|
715
|
+
disabledFeatures.push(feature);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (seen.has(arg)) continue;
|
|
721
|
+
seen.add(arg);
|
|
722
|
+
result.push(arg);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return result.map((arg) => (
|
|
726
|
+
arg === null
|
|
727
|
+
? `${disableFeaturesPrefix}${disabledFeatures.join(",")}`
|
|
728
|
+
: arg
|
|
729
|
+
));
|
|
730
|
+
}
|
|
731
|
+
|
|
628
732
|
export function buildBossChromeLaunchArgs({
|
|
629
733
|
port = DEFAULT_CHROME_PORT,
|
|
630
734
|
userDataDir = "",
|
|
@@ -642,7 +746,356 @@ export function buildBossChromeLaunchArgs({
|
|
|
642
746
|
"--new-window",
|
|
643
747
|
url
|
|
644
748
|
];
|
|
645
|
-
return
|
|
749
|
+
return normalizeChromeLaunchArgs(args);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function execFileText(file, args = [], { timeoutMs = 5000, maxBuffer = 1024 * 1024 } = {}) {
|
|
753
|
+
return new Promise((resolve) => {
|
|
754
|
+
execFile(file, args, {
|
|
755
|
+
timeout: timeoutMs,
|
|
756
|
+
maxBuffer,
|
|
757
|
+
windowsHide: true
|
|
758
|
+
}, (error, stdout, stderr) => {
|
|
759
|
+
resolve({
|
|
760
|
+
ok: !error,
|
|
761
|
+
stdout: String(stdout || ""),
|
|
762
|
+
stderr: String(stderr || ""),
|
|
763
|
+
error: error?.message || ""
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function inspectChromeCommandLineViaCdp({
|
|
770
|
+
host = DEFAULT_CHROME_HOST,
|
|
771
|
+
port = DEFAULT_CHROME_PORT
|
|
772
|
+
} = {}) {
|
|
773
|
+
let client = null;
|
|
774
|
+
try {
|
|
775
|
+
client = await CDP({ host, port });
|
|
776
|
+
const result = await client.Browser.getBrowserCommandLine();
|
|
777
|
+
const args = parseChromeCommandLineArgs(result?.arguments || result?.commandLine || result?.command_line || []);
|
|
778
|
+
if (args.length === 0) {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
source: "cdp_browser_command_line",
|
|
782
|
+
arguments: [],
|
|
783
|
+
error: "Browser.getBrowserCommandLine returned no command-line arguments"
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
ok: true,
|
|
788
|
+
source: "cdp_browser_command_line",
|
|
789
|
+
arguments: args
|
|
790
|
+
};
|
|
791
|
+
} catch (error) {
|
|
792
|
+
return {
|
|
793
|
+
ok: false,
|
|
794
|
+
source: "cdp_browser_command_line",
|
|
795
|
+
arguments: [],
|
|
796
|
+
error: error?.message || String(error || "")
|
|
797
|
+
};
|
|
798
|
+
} finally {
|
|
799
|
+
if (client) {
|
|
800
|
+
await client.close().catch(() => {});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function parseWindowsProcessListJson(text = "") {
|
|
806
|
+
const trimmed = String(text || "").trim();
|
|
807
|
+
if (!trimmed) return [];
|
|
808
|
+
const parsed = JSON.parse(trimmed);
|
|
809
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
810
|
+
return items
|
|
811
|
+
.map((item) => ({
|
|
812
|
+
pid: Number(item?.ProcessId),
|
|
813
|
+
command_line: String(item?.CommandLine || "")
|
|
814
|
+
}))
|
|
815
|
+
.filter((item) => Number.isFinite(item.pid) && item.command_line);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function parsePosixProcessList(text = "", port = DEFAULT_CHROME_PORT) {
|
|
819
|
+
const portPattern = new RegExp(`--remote-debugging-port(?:=|\\s+)${port}(?=\\s|$)`);
|
|
820
|
+
return String(text || "")
|
|
821
|
+
.split(/\r?\n/)
|
|
822
|
+
.map((line) => {
|
|
823
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
824
|
+
return match
|
|
825
|
+
? { pid: Number(match[1]), command_line: match[2] }
|
|
826
|
+
: null;
|
|
827
|
+
})
|
|
828
|
+
.filter((item) => item && Number.isFinite(item.pid) && portPattern.test(item.command_line));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function summarizeChromeProcesses(processes = []) {
|
|
832
|
+
return processes
|
|
833
|
+
.map((item) => ({
|
|
834
|
+
pid: item.pid,
|
|
835
|
+
command_line_length: String(item.command_line || "").length
|
|
836
|
+
}))
|
|
837
|
+
.filter((item) => Number.isFinite(item.pid));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function inspectChromeCommandLineViaProcessList({
|
|
841
|
+
port = DEFAULT_CHROME_PORT
|
|
842
|
+
} = {}) {
|
|
843
|
+
const portText = String(port);
|
|
844
|
+
let processes = [];
|
|
845
|
+
let raw = null;
|
|
846
|
+
|
|
847
|
+
if (process.platform === "win32") {
|
|
848
|
+
const portPattern = `--remote-debugging-port(=|\\s+)${portText}(\\s|$)`;
|
|
849
|
+
const script = [
|
|
850
|
+
"$items = Get-CimInstance Win32_Process",
|
|
851
|
+
`| Where-Object { $_.CommandLine -and $_.CommandLine -match '${portPattern}' }`,
|
|
852
|
+
"| Select-Object ProcessId,CommandLine;",
|
|
853
|
+
"$items | ConvertTo-Json -Compress"
|
|
854
|
+
].join(" ");
|
|
855
|
+
raw = await execFileText("powershell.exe", [
|
|
856
|
+
"-NoProfile",
|
|
857
|
+
"-ExecutionPolicy",
|
|
858
|
+
"Bypass",
|
|
859
|
+
"-Command",
|
|
860
|
+
script
|
|
861
|
+
], { timeoutMs: 6000 });
|
|
862
|
+
if (!raw.ok) {
|
|
863
|
+
return {
|
|
864
|
+
ok: false,
|
|
865
|
+
source: "process_list",
|
|
866
|
+
arguments: [],
|
|
867
|
+
processes: [],
|
|
868
|
+
error: raw.error || raw.stderr || "Failed to inspect Windows process list"
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
processes = parseWindowsProcessListJson(raw.stdout);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
return {
|
|
875
|
+
ok: false,
|
|
876
|
+
source: "process_list",
|
|
877
|
+
arguments: [],
|
|
878
|
+
processes: [],
|
|
879
|
+
error: `Failed to parse Windows process list: ${error?.message || error}`
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
const psArgs = process.platform === "darwin"
|
|
884
|
+
? ["-axo", "pid=,command="]
|
|
885
|
+
: ["-eo", "pid=,args="];
|
|
886
|
+
raw = await execFileText("ps", psArgs, { timeoutMs: 6000 });
|
|
887
|
+
if (!raw.ok) {
|
|
888
|
+
return {
|
|
889
|
+
ok: false,
|
|
890
|
+
source: "process_list",
|
|
891
|
+
arguments: [],
|
|
892
|
+
processes: [],
|
|
893
|
+
error: raw.error || raw.stderr || "Failed to inspect process list"
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
processes = parsePosixProcessList(raw.stdout, port);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (processes.length === 0) {
|
|
900
|
+
return {
|
|
901
|
+
ok: false,
|
|
902
|
+
source: "process_list",
|
|
903
|
+
arguments: [],
|
|
904
|
+
processes: [],
|
|
905
|
+
error: `No local process was found for --remote-debugging-port=${port}`
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const primary = processes[0];
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
source: "process_list",
|
|
912
|
+
arguments: parseChromeCommandLineArgs(primary.command_line),
|
|
913
|
+
process: {
|
|
914
|
+
pid: primary.pid,
|
|
915
|
+
command_line_length: primary.command_line.length
|
|
916
|
+
},
|
|
917
|
+
processes: summarizeChromeProcesses(processes)
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export async function inspectChromeDebugCommandLine({
|
|
922
|
+
host = DEFAULT_CHROME_HOST,
|
|
923
|
+
port = DEFAULT_CHROME_PORT,
|
|
924
|
+
_deps = {}
|
|
925
|
+
} = {}) {
|
|
926
|
+
const inspectViaCdp = _deps.inspectChromeCommandLineViaCdpImpl || inspectChromeCommandLineViaCdp;
|
|
927
|
+
const inspectViaProcess = _deps.inspectChromeCommandLineViaProcessListImpl || inspectChromeCommandLineViaProcessList;
|
|
928
|
+
const cdpResult = await inspectViaCdp({ host, port });
|
|
929
|
+
if (cdpResult?.ok && cdpResult.arguments?.length) {
|
|
930
|
+
return cdpResult;
|
|
931
|
+
}
|
|
932
|
+
if (!isLocalChromeHost(host)) {
|
|
933
|
+
return {
|
|
934
|
+
ok: false,
|
|
935
|
+
source: cdpResult?.source || "unknown",
|
|
936
|
+
arguments: [],
|
|
937
|
+
error: cdpResult?.error || `Cannot inspect process list for non-local Chrome debug host: ${host}`
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
const processResult = await inspectViaProcess({ port });
|
|
941
|
+
if (processResult?.ok && processResult.arguments?.length) {
|
|
942
|
+
return {
|
|
943
|
+
...processResult,
|
|
944
|
+
cdp_error: cdpResult?.error || null
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
ok: false,
|
|
949
|
+
source: processResult?.source || cdpResult?.source || "unknown",
|
|
950
|
+
arguments: [],
|
|
951
|
+
processes: processResult?.processes || [],
|
|
952
|
+
error: processResult?.error || cdpResult?.error || "Chrome command line could not be inspected"
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function waitForChromeDebugPortClosed({
|
|
957
|
+
host = DEFAULT_CHROME_HOST,
|
|
958
|
+
port = DEFAULT_CHROME_PORT,
|
|
959
|
+
timeoutMs = 6000,
|
|
960
|
+
intervalMs = 300,
|
|
961
|
+
listChromeTargetsImpl = listChromeTargets
|
|
962
|
+
} = {}) {
|
|
963
|
+
const started = Date.now();
|
|
964
|
+
let lastError = null;
|
|
965
|
+
let lastTargetCount = 0;
|
|
966
|
+
while (Date.now() - started <= timeoutMs) {
|
|
967
|
+
try {
|
|
968
|
+
const targets = await listChromeTargetsImpl({ host, port });
|
|
969
|
+
lastTargetCount = Array.isArray(targets) ? targets.length : 0;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (isChromeDebugUnavailableError(error)) {
|
|
972
|
+
return {
|
|
973
|
+
ok: true,
|
|
974
|
+
elapsed_ms: Date.now() - started
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
lastError = error;
|
|
978
|
+
}
|
|
979
|
+
await sleep(intervalMs);
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
ok: false,
|
|
983
|
+
elapsed_ms: Date.now() - started,
|
|
984
|
+
target_count: lastTargetCount,
|
|
985
|
+
error: lastError?.message || `Chrome debug port ${port} is still reachable`
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export async function closeChromeDebugInstance({
|
|
990
|
+
host = DEFAULT_CHROME_HOST,
|
|
991
|
+
port = DEFAULT_CHROME_PORT,
|
|
992
|
+
processes = [],
|
|
993
|
+
timeoutMs = 8000,
|
|
994
|
+
intervalMs = 300,
|
|
995
|
+
_deps = {}
|
|
996
|
+
} = {}) {
|
|
997
|
+
if (!isLocalChromeHost(host)) {
|
|
998
|
+
return {
|
|
999
|
+
ok: false,
|
|
1000
|
+
method: "none",
|
|
1001
|
+
error: `Refusing to close non-local Chrome debug host: ${host}`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
|
|
1006
|
+
const waitClosed = _deps.waitForChromeDebugPortClosedImpl || waitForChromeDebugPortClosed;
|
|
1007
|
+
let browserCloseAttempted = false;
|
|
1008
|
+
let browserCloseError = null;
|
|
1009
|
+
try {
|
|
1010
|
+
let client = null;
|
|
1011
|
+
try {
|
|
1012
|
+
client = await CDP({ host, port });
|
|
1013
|
+
if (typeof client?.Browser?.close !== "function") {
|
|
1014
|
+
throw new Error("Browser.close is not available");
|
|
1015
|
+
}
|
|
1016
|
+
browserCloseAttempted = true;
|
|
1017
|
+
await client.Browser.close();
|
|
1018
|
+
} finally {
|
|
1019
|
+
if (client) await client.close().catch(() => {});
|
|
1020
|
+
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
browserCloseError = error?.message || String(error || "");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
let closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
|
|
1026
|
+
if (closed.ok) {
|
|
1027
|
+
return {
|
|
1028
|
+
ok: true,
|
|
1029
|
+
method: browserCloseAttempted ? "Browser.close" : "port_already_closed",
|
|
1030
|
+
elapsed_ms: closed.elapsed_ms,
|
|
1031
|
+
browser_close_error: browserCloseError
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const pids = Array.from(new Set((processes || [])
|
|
1036
|
+
.map((item) => Number(item?.pid))
|
|
1037
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid)));
|
|
1038
|
+
const killedPids = [];
|
|
1039
|
+
const processErrors = [];
|
|
1040
|
+
for (const pid of pids) {
|
|
1041
|
+
try {
|
|
1042
|
+
process.kill(pid);
|
|
1043
|
+
killedPids.push(pid);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
processErrors.push({
|
|
1046
|
+
pid,
|
|
1047
|
+
error: error?.message || String(error || "")
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (killedPids.length > 0) {
|
|
1053
|
+
closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
|
|
1054
|
+
if (closed.ok) {
|
|
1055
|
+
return {
|
|
1056
|
+
ok: true,
|
|
1057
|
+
method: browserCloseAttempted ? "Browser.close+process.kill" : "process.kill",
|
|
1058
|
+
elapsed_ms: closed.elapsed_ms,
|
|
1059
|
+
killed_pids: killedPids,
|
|
1060
|
+
browser_close_error: browserCloseError,
|
|
1061
|
+
process_errors: processErrors
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
ok: false,
|
|
1068
|
+
method: browserCloseAttempted && killedPids.length > 0
|
|
1069
|
+
? "Browser.close+process.kill"
|
|
1070
|
+
: browserCloseAttempted
|
|
1071
|
+
? "Browser.close"
|
|
1072
|
+
: killedPids.length > 0
|
|
1073
|
+
? "process.kill"
|
|
1074
|
+
: "none",
|
|
1075
|
+
killed_pids: killedPids,
|
|
1076
|
+
browser_close_error: browserCloseError,
|
|
1077
|
+
process_errors: processErrors,
|
|
1078
|
+
wait: closed,
|
|
1079
|
+
error: closed.error || browserCloseError || "Failed to close Chrome debug instance"
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function summarizeRelaunch(result = {}, reason = "") {
|
|
1084
|
+
return {
|
|
1085
|
+
reason,
|
|
1086
|
+
launched: Boolean(result?.launched),
|
|
1087
|
+
chrome_path: result?.chrome_path || null,
|
|
1088
|
+
user_data_dir: result?.user_data_dir || null,
|
|
1089
|
+
launch_args: Array.isArray(result?.launch_args) ? result.launch_args : [],
|
|
1090
|
+
readiness: result?.readiness || null
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function createChromeGuardError(message, code, chromeGuard) {
|
|
1095
|
+
const error = new Error(message);
|
|
1096
|
+
error.code = code;
|
|
1097
|
+
error.chrome_guard = chromeGuard;
|
|
1098
|
+
return error;
|
|
646
1099
|
}
|
|
647
1100
|
|
|
648
1101
|
export async function waitForChromeDebugPort({
|
|
@@ -677,7 +1130,8 @@ export async function launchChromeDebugInstance({
|
|
|
677
1130
|
host = DEFAULT_CHROME_HOST,
|
|
678
1131
|
port = DEFAULT_CHROME_PORT,
|
|
679
1132
|
url = "about:blank",
|
|
680
|
-
slowLive = false
|
|
1133
|
+
slowLive = false,
|
|
1134
|
+
userDataDir = ""
|
|
681
1135
|
} = {}) {
|
|
682
1136
|
if (!isLocalChromeHost(host)) {
|
|
683
1137
|
throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
|
|
@@ -686,8 +1140,9 @@ export async function launchChromeDebugInstance({
|
|
|
686
1140
|
if (!chromePath) {
|
|
687
1141
|
throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
|
|
688
1142
|
}
|
|
689
|
-
const
|
|
690
|
-
|
|
1143
|
+
const resolvedUserDataDir = userDataDir || getBossChromeUserDataDir(port);
|
|
1144
|
+
ensureDir(resolvedUserDataDir);
|
|
1145
|
+
const args = buildBossChromeLaunchArgs({ port, userDataDir: resolvedUserDataDir, url });
|
|
691
1146
|
const child = spawn(chromePath, args, {
|
|
692
1147
|
detached: true,
|
|
693
1148
|
stdio: "ignore",
|
|
@@ -706,7 +1161,7 @@ export async function launchChromeDebugInstance({
|
|
|
706
1161
|
return {
|
|
707
1162
|
launched: true,
|
|
708
1163
|
chrome_path: chromePath,
|
|
709
|
-
user_data_dir:
|
|
1164
|
+
user_data_dir: resolvedUserDataDir,
|
|
710
1165
|
launch_args: args,
|
|
711
1166
|
port,
|
|
712
1167
|
url,
|
|
@@ -722,21 +1177,168 @@ export async function ensureChromeDebugPort({
|
|
|
722
1177
|
port = DEFAULT_CHROME_PORT,
|
|
723
1178
|
url = "about:blank",
|
|
724
1179
|
slowLive = false,
|
|
725
|
-
launchIfMissing = true
|
|
1180
|
+
launchIfMissing = true,
|
|
1181
|
+
userDataDir = "",
|
|
1182
|
+
enforceRequiredFlags = true,
|
|
1183
|
+
requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS,
|
|
1184
|
+
_deps = {}
|
|
726
1185
|
} = {}) {
|
|
1186
|
+
const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
|
|
1187
|
+
const inspectCommandLineImpl = _deps.inspectChromeDebugCommandLineImpl || inspectChromeDebugCommandLine;
|
|
1188
|
+
const closeChromeDebugInstanceImpl = _deps.closeChromeDebugInstanceImpl || closeChromeDebugInstance;
|
|
1189
|
+
const launchChromeDebugInstanceImpl = _deps.launchChromeDebugInstanceImpl || launchChromeDebugInstance;
|
|
1190
|
+
const required = Array.from(new Set((requiredFlags || []).filter(Boolean)));
|
|
1191
|
+
const baseGuard = {
|
|
1192
|
+
guard_checked: Boolean(enforceRequiredFlags),
|
|
1193
|
+
required_flags: required,
|
|
1194
|
+
missing_flags: [],
|
|
1195
|
+
required_flags_ok: !enforceRequiredFlags,
|
|
1196
|
+
replaced: false,
|
|
1197
|
+
close_method: null,
|
|
1198
|
+
relaunch: null,
|
|
1199
|
+
host,
|
|
1200
|
+
port
|
|
1201
|
+
};
|
|
1202
|
+
|
|
727
1203
|
try {
|
|
728
|
-
const targets = await
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1204
|
+
const targets = await listChromeTargetsImpl({ host, port });
|
|
1205
|
+
if (!enforceRequiredFlags) {
|
|
1206
|
+
return {
|
|
1207
|
+
launched: false,
|
|
1208
|
+
reused: true,
|
|
1209
|
+
port,
|
|
1210
|
+
target_count: targets.length,
|
|
1211
|
+
...baseGuard
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const commandLine = await inspectCommandLineImpl({ host, port, _deps });
|
|
1216
|
+
const missingFlags = commandLine?.ok
|
|
1217
|
+
? getMissingRequiredChromeFlags(commandLine.arguments, required)
|
|
1218
|
+
: required.slice();
|
|
1219
|
+
const commandLineEvidence = {
|
|
1220
|
+
command_line_source: commandLine?.source || "unknown",
|
|
1221
|
+
command_line_error: commandLine?.ok ? null : (commandLine?.error || "Chrome command line could not be inspected"),
|
|
1222
|
+
command_line_args_count: Array.isArray(commandLine?.arguments) ? commandLine.arguments.length : 0,
|
|
1223
|
+
inspected_process: commandLine?.process || null,
|
|
1224
|
+
inspected_processes: commandLine?.processes || []
|
|
734
1225
|
};
|
|
1226
|
+
if (missingFlags.length === 0) {
|
|
1227
|
+
return {
|
|
1228
|
+
launched: false,
|
|
1229
|
+
reused: true,
|
|
1230
|
+
port,
|
|
1231
|
+
target_count: targets.length,
|
|
1232
|
+
...baseGuard,
|
|
1233
|
+
required_flags_ok: true,
|
|
1234
|
+
...commandLineEvidence
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const guard = {
|
|
1239
|
+
...baseGuard,
|
|
1240
|
+
required_flags_ok: false,
|
|
1241
|
+
missing_flags: missingFlags,
|
|
1242
|
+
target_count: targets.length,
|
|
1243
|
+
...commandLineEvidence
|
|
1244
|
+
};
|
|
1245
|
+
if (!isLocalChromeHost(host)) {
|
|
1246
|
+
throw createChromeGuardError(
|
|
1247
|
+
`Chrome debug host ${host}:${port} is missing required Chrome flags and is not local, so it will not be auto-closed.`,
|
|
1248
|
+
"CHROME_REQUIRED_FLAGS_MISSING_NON_LOCAL",
|
|
1249
|
+
guard
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const closeResult = await closeChromeDebugInstanceImpl({
|
|
1254
|
+
host,
|
|
1255
|
+
port,
|
|
1256
|
+
processes: commandLine?.processes || [],
|
|
1257
|
+
_deps
|
|
1258
|
+
});
|
|
1259
|
+
if (!closeResult?.ok) {
|
|
1260
|
+
throw createChromeGuardError(
|
|
1261
|
+
`Chrome debug instance on port ${port} is missing required flags and could not be closed: ${closeResult?.error || "unknown close failure"}`,
|
|
1262
|
+
"CHROME_REQUIRED_FLAGS_REPLACE_FAILED",
|
|
1263
|
+
{
|
|
1264
|
+
...guard,
|
|
1265
|
+
close_method: closeResult?.method || null,
|
|
1266
|
+
close_result: closeResult || null
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
const relaunch = await launchChromeDebugInstanceImpl({
|
|
1273
|
+
host,
|
|
1274
|
+
port,
|
|
1275
|
+
url,
|
|
1276
|
+
slowLive,
|
|
1277
|
+
userDataDir
|
|
1278
|
+
});
|
|
1279
|
+
return {
|
|
1280
|
+
...relaunch,
|
|
1281
|
+
reused: false,
|
|
1282
|
+
...guard,
|
|
1283
|
+
required_flags_ok: true,
|
|
1284
|
+
replaced: true,
|
|
1285
|
+
close_method: closeResult.method || null,
|
|
1286
|
+
close_result: closeResult,
|
|
1287
|
+
relaunch: summarizeRelaunch(relaunch, "missing_required_flags")
|
|
1288
|
+
};
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
throw createChromeGuardError(
|
|
1291
|
+
`Chrome debug instance on port ${port} was closed for missing flags, but relaunch failed: ${error?.message || error}`,
|
|
1292
|
+
"CHROME_REQUIRED_FLAGS_RELAUNCH_FAILED",
|
|
1293
|
+
{
|
|
1294
|
+
...guard,
|
|
1295
|
+
close_method: closeResult.method || null,
|
|
1296
|
+
close_result: closeResult,
|
|
1297
|
+
relaunch: {
|
|
1298
|
+
reason: "missing_required_flags",
|
|
1299
|
+
launched: false,
|
|
1300
|
+
error: error?.message || String(error || "")
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
735
1305
|
} catch (error) {
|
|
1306
|
+
if (error?.chrome_guard) {
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
736
1309
|
if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
|
|
737
1310
|
throw error;
|
|
738
1311
|
}
|
|
739
|
-
|
|
1312
|
+
try {
|
|
1313
|
+
const relaunch = await launchChromeDebugInstanceImpl({
|
|
1314
|
+
host,
|
|
1315
|
+
port,
|
|
1316
|
+
url,
|
|
1317
|
+
slowLive,
|
|
1318
|
+
userDataDir
|
|
1319
|
+
});
|
|
1320
|
+
return {
|
|
1321
|
+
...baseGuard,
|
|
1322
|
+
...relaunch,
|
|
1323
|
+
reused: false,
|
|
1324
|
+
required_flags_ok: true,
|
|
1325
|
+
relaunch: summarizeRelaunch(relaunch, "port_unreachable")
|
|
1326
|
+
};
|
|
1327
|
+
} catch (launchError) {
|
|
1328
|
+
throw createChromeGuardError(
|
|
1329
|
+
`Chrome debug port ${port} was unreachable and Chrome relaunch failed: ${launchError?.message || launchError}`,
|
|
1330
|
+
"CHROME_RELAUNCH_FAILED",
|
|
1331
|
+
{
|
|
1332
|
+
...baseGuard,
|
|
1333
|
+
required_flags_ok: false,
|
|
1334
|
+
relaunch: {
|
|
1335
|
+
reason: "port_unreachable",
|
|
1336
|
+
launched: false,
|
|
1337
|
+
error: launchError?.message || String(launchError || "")
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
740
1342
|
}
|
|
741
1343
|
}
|
|
742
1344
|
|
|
@@ -784,21 +1386,25 @@ export async function connectToChromeTargetOrOpen({
|
|
|
784
1386
|
targetUrl,
|
|
785
1387
|
allowNavigate = true,
|
|
786
1388
|
slowLive = false,
|
|
787
|
-
launchIfMissing = true
|
|
1389
|
+
launchIfMissing = true,
|
|
1390
|
+
_deps = {}
|
|
788
1391
|
} = {}) {
|
|
1392
|
+
const ensureChromeDebugPortImpl = _deps.ensureChromeDebugPortImpl || ensureChromeDebugPort;
|
|
1393
|
+
const connectToChromeTargetImpl = _deps.connectToChromeTargetImpl || connectToChromeTarget;
|
|
1394
|
+
const openChromeTargetImpl = _deps.openChromeTargetImpl || openChromeTarget;
|
|
789
1395
|
let chrome = null;
|
|
790
|
-
if (
|
|
791
|
-
chrome = await
|
|
1396
|
+
if (targetUrl) {
|
|
1397
|
+
chrome = await ensureChromeDebugPortImpl({
|
|
792
1398
|
host,
|
|
793
1399
|
port,
|
|
794
1400
|
url: targetUrl,
|
|
795
1401
|
slowLive,
|
|
796
|
-
launchIfMissing
|
|
1402
|
+
launchIfMissing: allowNavigate && launchIfMissing
|
|
797
1403
|
});
|
|
798
1404
|
}
|
|
799
1405
|
|
|
800
1406
|
try {
|
|
801
|
-
const session = await
|
|
1407
|
+
const session = await connectToChromeTargetImpl({
|
|
802
1408
|
host,
|
|
803
1409
|
port,
|
|
804
1410
|
targetUrlIncludes,
|
|
@@ -816,7 +1422,7 @@ export async function connectToChromeTargetOrOpen({
|
|
|
816
1422
|
|
|
817
1423
|
if (typeof fallbackTargetPredicate === "function") {
|
|
818
1424
|
try {
|
|
819
|
-
const session = await
|
|
1425
|
+
const session = await connectToChromeTargetImpl({
|
|
820
1426
|
host,
|
|
821
1427
|
port,
|
|
822
1428
|
targetPredicate: fallbackTargetPredicate
|
|
@@ -834,9 +1440,9 @@ export async function connectToChromeTargetOrOpen({
|
|
|
834
1440
|
|
|
835
1441
|
let openAttempt = null;
|
|
836
1442
|
if (targetUrl) {
|
|
837
|
-
openAttempt = await
|
|
1443
|
+
openAttempt = await openChromeTargetImpl({ host, port, url: targetUrl });
|
|
838
1444
|
if (openAttempt.ok) {
|
|
839
|
-
const session = await
|
|
1445
|
+
const session = await connectToChromeTargetImpl({
|
|
840
1446
|
host,
|
|
841
1447
|
port,
|
|
842
1448
|
targetPredicate: (target) => (
|
|
@@ -856,7 +1462,7 @@ export async function connectToChromeTargetOrOpen({
|
|
|
856
1462
|
}
|
|
857
1463
|
}
|
|
858
1464
|
|
|
859
|
-
const session = await
|
|
1465
|
+
const session = await connectToChromeTargetImpl({
|
|
860
1466
|
host,
|
|
861
1467
|
port,
|
|
862
1468
|
targetPredicate: (target) => target?.type === "page"
|